refactor: Enhance OverworldEditor with Dynamic Context Menu and Tile Selector Widget
- Replaced static context menu setup in OverworldEditor with dynamic configuration based on the current map state, improving usability and responsiveness. - Introduced TileSelectorWidget for better tile selection management, allowing for a more intuitive user experience when selecting tiles. - Updated canvas controls to include zoom in and zoom out functionalities, enhancing the editor's navigation capabilities. - Cleaned up legacy context menu code and improved overall organization for better maintainability and clarity.
This commit is contained in:
@@ -453,20 +453,29 @@ void MapPropertiesSystem::SetupCanvasContextMenu(
|
||||
}
|
||||
|
||||
// Canvas controls
|
||||
gui::Canvas::ContextMenuItem reset_pos_item;
|
||||
reset_pos_item.label = "Reset Canvas Position";
|
||||
reset_pos_item.callback = [&canvas]() {
|
||||
canvas.set_scrolling(ImVec2(0, 0));
|
||||
};
|
||||
canvas.AddContextMenuItem(reset_pos_item);
|
||||
|
||||
gui::Canvas::ContextMenuItem zoom_fit_item;
|
||||
zoom_fit_item.label = "Zoom to Fit";
|
||||
zoom_fit_item.callback = [&canvas]() {
|
||||
gui::Canvas::ContextMenuItem reset_view_item;
|
||||
reset_view_item.label = ICON_MD_RESTORE " Reset View";
|
||||
reset_view_item.callback = [&canvas]() {
|
||||
canvas.set_global_scale(1.0f);
|
||||
canvas.set_scrolling(ImVec2(0, 0));
|
||||
};
|
||||
canvas.AddContextMenuItem(zoom_fit_item);
|
||||
canvas.AddContextMenuItem(reset_view_item);
|
||||
|
||||
gui::Canvas::ContextMenuItem zoom_in_item;
|
||||
zoom_in_item.label = ICON_MD_ZOOM_IN " Zoom In";
|
||||
zoom_in_item.callback = [&canvas]() {
|
||||
float scale = std::min(2.0f, canvas.global_scale() + 0.25f);
|
||||
canvas.set_global_scale(scale);
|
||||
};
|
||||
canvas.AddContextMenuItem(zoom_in_item);
|
||||
|
||||
gui::Canvas::ContextMenuItem zoom_out_item;
|
||||
zoom_out_item.label = ICON_MD_ZOOM_OUT " Zoom Out";
|
||||
zoom_out_item.callback = [&canvas]() {
|
||||
float scale = std::max(0.25f, canvas.global_scale() - 0.25f);
|
||||
canvas.set_global_scale(scale);
|
||||
};
|
||||
canvas.AddContextMenuItem(zoom_out_item);
|
||||
}
|
||||
|
||||
// Private method implementations
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "app/editor/overworld/tile16_editor.h"
|
||||
#include "app/gfx/arena.h"
|
||||
#include "app/gfx/bitmap.h"
|
||||
#include "app/gui/widgets/tile_selector_widget.h"
|
||||
#include "app/gfx/performance_profiler.h"
|
||||
#include "app/gfx/snes_palette.h"
|
||||
#include "app/gfx/tilemap.h"
|
||||
@@ -64,8 +65,8 @@ void OverworldEditor::Initialize() {
|
||||
entity_renderer_ = std::make_unique<OverworldEntityRenderer>(
|
||||
&overworld_, &ow_map_canvas_, &sprite_previews_);
|
||||
|
||||
// Setup overworld canvas context menu
|
||||
SetupOverworldCanvasContextMenu();
|
||||
// Note: Context menu is now setup dynamically in DrawOverworldCanvas()
|
||||
// for context-aware menu items based on current map state
|
||||
|
||||
// Old toolset initialization removed - using modern CompactToolbar instead
|
||||
}
|
||||
@@ -325,15 +326,6 @@ void OverworldEditor::DrawToolset() {
|
||||
ImGui::OpenPopup("UpgradeROMVersion");
|
||||
});
|
||||
|
||||
// World selector
|
||||
const char* worlds[] = {"Light", "Dark", "Extra"};
|
||||
if (toolbar.AddCombo(ICON_MD_PUBLIC, ¤t_world_, worlds, 3)) {
|
||||
RefreshMapProperties();
|
||||
RefreshOverworldMap();
|
||||
}
|
||||
|
||||
toolbar.AddSeparator();
|
||||
|
||||
// Inline map properties with icon labels - use toolbar methods for consistency
|
||||
if (toolbar.AddProperty(ICON_MD_IMAGE, " Gfx",
|
||||
overworld_.mutable_overworld_map(current_map_)->mutable_area_graphics(),
|
||||
@@ -709,7 +701,8 @@ void OverworldEditor::CheckForOverworldEdits() {
|
||||
|
||||
// User has selected a tile they want to draw from the blockset
|
||||
// and clicked on the canvas.
|
||||
if (!blockset_canvas_.points().empty() &&
|
||||
// Note: With TileSelectorWidget, we check if a valid tile is selected instead of canvas points
|
||||
if (current_tile16_ >= 0 &&
|
||||
!ow_map_canvas_.select_rect_active() &&
|
||||
ow_map_canvas_.DrawTilemapPainter(tile16_blockset_, current_tile16_)) {
|
||||
DrawOverworldEdits();
|
||||
@@ -1099,19 +1092,136 @@ absl::Status OverworldEditor::CheckForCurrentMap() {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
void OverworldEditor::CheckForMousePan() {
|
||||
// Overworld Canvas Pan/Zoom Helpers
|
||||
|
||||
namespace {
|
||||
|
||||
// Calculate the total canvas content size based on world layout
|
||||
ImVec2 CalculateOverworldContentSize(float scale) {
|
||||
// 8x8 grid of 512x512 maps = 4096x4096 total
|
||||
constexpr float kWorldSize = 512.0f * 8.0f; // 4096
|
||||
return ImVec2(kWorldSize * scale, kWorldSize * scale);
|
||||
}
|
||||
|
||||
// Clamp scroll position to valid bounds
|
||||
ImVec2 ClampScrollPosition(ImVec2 scroll, ImVec2 content_size, ImVec2 visible_size) {
|
||||
// Calculate maximum scroll values
|
||||
float max_scroll_x = std::max(0.0f, content_size.x - visible_size.x);
|
||||
float max_scroll_y = std::max(0.0f, content_size.y - visible_size.y);
|
||||
|
||||
// Clamp to valid range [min_scroll, 0]
|
||||
// Note: Canvas uses negative scrolling for right/down
|
||||
float clamped_x = std::clamp(scroll.x, -max_scroll_x, 0.0f);
|
||||
float clamped_y = std::clamp(scroll.y, -max_scroll_y, 0.0f);
|
||||
|
||||
return ImVec2(clamped_x, clamped_y);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void OverworldEditor::HandleOverworldPan() {
|
||||
// Middle mouse button panning
|
||||
if (ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) {
|
||||
previous_mode = current_mode;
|
||||
current_mode = EditingMode::PAN;
|
||||
ow_map_canvas_.set_draggable(true);
|
||||
middle_mouse_dragging_ = true;
|
||||
if (!middle_mouse_dragging_) {
|
||||
previous_mode = current_mode;
|
||||
current_mode = EditingMode::PAN;
|
||||
middle_mouse_dragging_ = true;
|
||||
}
|
||||
|
||||
// Get mouse delta and apply to scroll
|
||||
ImVec2 mouse_delta = ImGui::GetIO().MouseDelta;
|
||||
ImVec2 current_scroll = ow_map_canvas_.scrolling();
|
||||
ImVec2 new_scroll = ImVec2(
|
||||
current_scroll.x + mouse_delta.x,
|
||||
current_scroll.y + mouse_delta.y
|
||||
);
|
||||
|
||||
// Clamp scroll to boundaries
|
||||
ImVec2 content_size = CalculateOverworldContentSize(ow_map_canvas_.global_scale());
|
||||
ImVec2 visible_size = ow_map_canvas_.canvas_size();
|
||||
new_scroll = ClampScrollPosition(new_scroll, content_size, visible_size);
|
||||
|
||||
ow_map_canvas_.set_scrolling(new_scroll);
|
||||
}
|
||||
if (ImGui::IsMouseReleased(ImGuiMouseButton_Middle) &&
|
||||
current_mode == EditingMode::PAN && middle_mouse_dragging_) {
|
||||
|
||||
if (ImGui::IsMouseReleased(ImGuiMouseButton_Middle) && middle_mouse_dragging_) {
|
||||
current_mode = previous_mode;
|
||||
ow_map_canvas_.set_draggable(false);
|
||||
middle_mouse_dragging_ = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void OverworldEditor::HandleOverworldZoom() {
|
||||
if (!ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
// Mouse wheel zoom with Ctrl key
|
||||
if (io.MouseWheel != 0.0f && io.KeyCtrl) {
|
||||
float current_scale = ow_map_canvas_.global_scale();
|
||||
float zoom_delta = io.MouseWheel * 0.1f;
|
||||
float new_scale = current_scale + zoom_delta;
|
||||
|
||||
// Clamp zoom range (0.25x to 2.0x)
|
||||
new_scale = std::clamp(new_scale, 0.25f, 2.0f);
|
||||
|
||||
if (new_scale != current_scale) {
|
||||
// Get mouse position relative to canvas
|
||||
ImVec2 mouse_pos_canvas = ImVec2(
|
||||
io.MousePos.x - ow_map_canvas_.zero_point().x,
|
||||
io.MousePos.y - ow_map_canvas_.zero_point().y
|
||||
);
|
||||
|
||||
// Calculate content position under mouse before zoom
|
||||
ImVec2 scroll = ow_map_canvas_.scrolling();
|
||||
ImVec2 content_pos_before = ImVec2(
|
||||
(mouse_pos_canvas.x - scroll.x) / current_scale,
|
||||
(mouse_pos_canvas.y - scroll.y) / current_scale
|
||||
);
|
||||
|
||||
// Apply new scale
|
||||
ow_map_canvas_.set_global_scale(new_scale);
|
||||
|
||||
// Calculate new scroll to keep same content under mouse
|
||||
ImVec2 new_scroll = ImVec2(
|
||||
mouse_pos_canvas.x - (content_pos_before.x * new_scale),
|
||||
mouse_pos_canvas.y - (content_pos_before.y * new_scale)
|
||||
);
|
||||
|
||||
// Clamp scroll to boundaries with new scale
|
||||
ImVec2 content_size = CalculateOverworldContentSize(new_scale);
|
||||
ImVec2 visible_size = ow_map_canvas_.canvas_size();
|
||||
new_scroll = ClampScrollPosition(new_scroll, content_size, visible_size);
|
||||
|
||||
ow_map_canvas_.set_scrolling(new_scroll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OverworldEditor::ResetOverworldView() {
|
||||
ow_map_canvas_.set_global_scale(1.0f);
|
||||
ow_map_canvas_.set_scrolling(ImVec2(0, 0));
|
||||
}
|
||||
|
||||
void OverworldEditor::CenterOverworldView() {
|
||||
float scale = ow_map_canvas_.global_scale();
|
||||
ImVec2 content_size = CalculateOverworldContentSize(scale);
|
||||
ImVec2 visible_size = ow_map_canvas_.canvas_size();
|
||||
|
||||
// Center the view
|
||||
ImVec2 centered_scroll = ImVec2(
|
||||
-(content_size.x - visible_size.x) / 2.0f,
|
||||
-(content_size.y - visible_size.y) / 2.0f
|
||||
);
|
||||
|
||||
ow_map_canvas_.set_scrolling(centered_scroll);
|
||||
}
|
||||
|
||||
void OverworldEditor::CheckForMousePan() {
|
||||
// Legacy wrapper - now calls HandleOverworldPan
|
||||
HandleOverworldPan();
|
||||
}
|
||||
|
||||
void OverworldEditor::DrawOverworldCanvas() {
|
||||
@@ -1129,12 +1239,38 @@ void OverworldEditor::DrawOverworldCanvas() {
|
||||
ow_map_canvas_.DrawBackground();
|
||||
gui::EndNoPadding();
|
||||
|
||||
CheckForMousePan();
|
||||
// Setup dynamic context menu based on current map state (Phase 3B)
|
||||
if (rom_->is_loaded() && overworld_.is_loaded() && map_properties_system_) {
|
||||
map_properties_system_->SetupCanvasContextMenu(
|
||||
ow_map_canvas_, current_map_, current_map_lock_,
|
||||
show_map_properties_panel_, show_custom_bg_color_editor_,
|
||||
show_overlay_editor_);
|
||||
}
|
||||
|
||||
// Handle pan and zoom
|
||||
HandleOverworldPan();
|
||||
HandleOverworldZoom();
|
||||
|
||||
if (current_mode == EditingMode::PAN) {
|
||||
// In PAN mode, allow right-click drag for panning
|
||||
if (ImGui::IsMouseDragging(ImGuiMouseButton_Right) && ImGui::IsItemHovered()) {
|
||||
ImVec2 mouse_delta = ImGui::GetIO().MouseDelta;
|
||||
ImVec2 current_scroll = ow_map_canvas_.scrolling();
|
||||
ImVec2 new_scroll = ImVec2(
|
||||
current_scroll.x + mouse_delta.x,
|
||||
current_scroll.y + mouse_delta.y
|
||||
);
|
||||
|
||||
// Clamp scroll to boundaries
|
||||
ImVec2 content_size = CalculateOverworldContentSize(ow_map_canvas_.global_scale());
|
||||
ImVec2 visible_size = ow_map_canvas_.canvas_size();
|
||||
new_scroll = ClampScrollPosition(new_scroll, content_size, visible_size);
|
||||
|
||||
ow_map_canvas_.set_scrolling(new_scroll);
|
||||
}
|
||||
ow_map_canvas_.DrawContextMenu();
|
||||
} else {
|
||||
ow_map_canvas_.set_draggable(false);
|
||||
// Handle map interaction with middle-click instead of right-click
|
||||
// Handle map interaction (tile painting, etc.)
|
||||
HandleMapInteraction();
|
||||
}
|
||||
|
||||
@@ -1175,63 +1311,46 @@ void OverworldEditor::DrawOverworldCanvas() {
|
||||
ow_map_canvas_.DrawGrid();
|
||||
ow_map_canvas_.DrawOverlay();
|
||||
EndChild();
|
||||
|
||||
// Handle mouse wheel activity
|
||||
if (ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows) &&
|
||||
ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) {
|
||||
ImGui::SetScrollX(ImGui::GetScrollX() + ImGui::GetIO().MouseWheelH * 16.0f);
|
||||
ImGui::SetScrollY(ImGui::GetScrollY() + ImGui::GetIO().MouseWheel * 16.0f);
|
||||
}
|
||||
}
|
||||
|
||||
absl::Status OverworldEditor::DrawTile16Selector() {
|
||||
gui::BeginPadding(3);
|
||||
ImGui::BeginGroup();
|
||||
gui::BeginChildWithScrollbar("##Tile16SelectorScrollRegion");
|
||||
blockset_canvas_.DrawBackground();
|
||||
gui::EndPadding(); // Fixed: was EndNoPadding()
|
||||
gui::EndPadding();
|
||||
|
||||
blockset_canvas_.DrawContextMenu();
|
||||
blockset_canvas_.DrawBitmap(tile16_blockset_.atlas, /*x_offset=*/2,
|
||||
map_blockset_loaded_, /*scale=*/2);
|
||||
bool tile_selected = false;
|
||||
if (!blockset_selector_) {
|
||||
gui::TileSelectorWidget::Config selector_config;
|
||||
selector_config.tile_size = 16;
|
||||
selector_config.display_scale = 2.0f;
|
||||
selector_config.tiles_per_row = 8;
|
||||
selector_config.total_tiles = zelda3::kNumTile16Individual;
|
||||
selector_config.draw_offset = ImVec2(2.0f, 0.0f);
|
||||
selector_config.highlight_color = ImVec4(0.95f, 0.75f, 0.3f, 1.0f);
|
||||
|
||||
// Call DrawTileSelector after event detection for visual feedback
|
||||
if (blockset_canvas_.DrawTileSelector(32.0f)) {
|
||||
tile_selected = true;
|
||||
blockset_selector_ = std::make_unique<gui::TileSelectorWidget>(
|
||||
"OwBlocksetSelector", selector_config);
|
||||
blockset_selector_->AttachCanvas(&blockset_canvas_);
|
||||
}
|
||||
|
||||
UpdateBlocksetSelectorState();
|
||||
|
||||
gfx::Bitmap& atlas = tile16_blockset_.atlas;
|
||||
bool atlas_ready = map_blockset_loaded_ && atlas.is_active();
|
||||
auto result = blockset_selector_->Render(atlas, atlas_ready);
|
||||
|
||||
if (result.selection_changed) {
|
||||
current_tile16_ = result.selected_tile;
|
||||
RETURN_IF_ERROR(tile16_editor_.SetCurrentTile(current_tile16_));
|
||||
// Note: We do NOT auto-scroll here because it breaks user interaction.
|
||||
// The canvas should only scroll when explicitly requested (e.g., when
|
||||
// selecting a tile from the overworld canvas via ScrollBlocksetCanvasToCurrentTile).
|
||||
}
|
||||
|
||||
if (result.tile_double_clicked) {
|
||||
show_tile16_editor_ = true;
|
||||
}
|
||||
|
||||
// Then check for single click (if not double-click)
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
||||
blockset_canvas_.IsMouseHovering()) {
|
||||
tile_selected = true;
|
||||
}
|
||||
|
||||
if (tile_selected) {
|
||||
// Get mouse position relative to canvas
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
ImVec2 canvas_pos = blockset_canvas_.zero_point();
|
||||
ImVec2 mouse_pos =
|
||||
ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
|
||||
|
||||
// Calculate grid position (32x32 tiles in blockset)
|
||||
int x_offset = static_cast<int>(mouse_pos.x / 32);
|
||||
int grid_y = static_cast<int>(mouse_pos.y / 32);
|
||||
int id = x_offset + grid_y * 8; // 8 tiles per row in blockset
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
blockset_canvas_.DrawGrid();
|
||||
blockset_canvas_.DrawOverlay();
|
||||
|
||||
EndChild();
|
||||
ImGui::EndGroup();
|
||||
return absl::OkStatus();
|
||||
@@ -2089,126 +2208,33 @@ void OverworldEditor::HandleMapInteraction() {
|
||||
}
|
||||
}
|
||||
|
||||
void OverworldEditor::SetupOverworldCanvasContextMenu() {
|
||||
// Clear any existing context menu items
|
||||
ow_map_canvas_.ClearContextMenuItems();
|
||||
|
||||
// Add overworld-specific context menu items
|
||||
gui::Canvas::ContextMenuItem lock_item;
|
||||
lock_item.label = current_map_lock_ ? "Unlock Map" : "Lock to This Map";
|
||||
lock_item.callback = [this]() {
|
||||
current_map_lock_ = !current_map_lock_;
|
||||
if (current_map_lock_) {
|
||||
// Get the current map from mouse position
|
||||
auto mouse_position = ow_map_canvas_.drawn_tile_position();
|
||||
int map_x = mouse_position.x / kOverworldMapSize;
|
||||
int map_y = mouse_position.y / kOverworldMapSize;
|
||||
int hovered_map = map_x + map_y * 8;
|
||||
if (current_world_ == 1) {
|
||||
hovered_map += 0x40;
|
||||
} else if (current_world_ == 2) {
|
||||
hovered_map += 0x80;
|
||||
}
|
||||
if (hovered_map >= 0 && hovered_map < 0xA0) {
|
||||
current_map_ = hovered_map;
|
||||
}
|
||||
}
|
||||
};
|
||||
ow_map_canvas_.AddContextMenuItem(lock_item);
|
||||
|
||||
// Map Properties
|
||||
gui::Canvas::ContextMenuItem properties_item;
|
||||
properties_item.label = "Map Properties";
|
||||
properties_item.callback = [this]() {
|
||||
show_map_properties_panel_ = true;
|
||||
};
|
||||
ow_map_canvas_.AddContextMenuItem(properties_item);
|
||||
|
||||
// Custom overworld features (only show if v3+)
|
||||
static uint8_t asm_version =
|
||||
(*rom_)[zelda3::OverworldCustomASMHasBeenApplied];
|
||||
if (asm_version >= 3 && asm_version != 0xFF) {
|
||||
// Custom Background Color
|
||||
gui::Canvas::ContextMenuItem bg_color_item;
|
||||
bg_color_item.label = "Custom Background Color";
|
||||
bg_color_item.callback = [this]() {
|
||||
show_custom_bg_color_editor_ = true;
|
||||
};
|
||||
ow_map_canvas_.AddContextMenuItem(bg_color_item);
|
||||
|
||||
// Overlay Settings
|
||||
gui::Canvas::ContextMenuItem overlay_item;
|
||||
overlay_item.label = "Overlay Settings";
|
||||
overlay_item.callback = [this]() {
|
||||
show_overlay_editor_ = true;
|
||||
};
|
||||
ow_map_canvas_.AddContextMenuItem(overlay_item);
|
||||
}
|
||||
|
||||
// Map editing controls
|
||||
gui::Canvas::ContextMenuItem refresh_map_item;
|
||||
refresh_map_item.label = "Refresh Map Changes";
|
||||
refresh_map_item.callback = [this]() {
|
||||
RefreshOverworldMap();
|
||||
auto status = RefreshTile16Blockset();
|
||||
if (!status.ok()) {
|
||||
LOG_ERROR("OverworldEditor", "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";
|
||||
reset_pos_item.callback = [this]() {
|
||||
ow_map_canvas_.set_scrolling(ImVec2(0, 0));
|
||||
};
|
||||
ow_map_canvas_.AddContextMenuItem(reset_pos_item);
|
||||
|
||||
gui::Canvas::ContextMenuItem zoom_fit_item;
|
||||
zoom_fit_item.label = "Zoom to Fit";
|
||||
zoom_fit_item.callback = [this]() {
|
||||
ow_map_canvas_.set_global_scale(1.0f);
|
||||
ow_map_canvas_.set_scrolling(ImVec2(0, 0));
|
||||
};
|
||||
ow_map_canvas_.AddContextMenuItem(zoom_fit_item);
|
||||
}
|
||||
// Note: SetupOverworldCanvasContextMenu has been removed (Phase 3B).
|
||||
// Context menu is now setup dynamically in DrawOverworldCanvas() via
|
||||
// MapPropertiesSystem::SetupCanvasContextMenu() for context-aware menu items.
|
||||
|
||||
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)
|
||||
if (blockset_selector_) {
|
||||
blockset_selector_->ScrollToTile(current_tile16_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: maintain legacy behavior when the selector is unavailable.
|
||||
constexpr int kTilesPerRow = 8;
|
||||
constexpr int kTileDisplaySize = 32;
|
||||
|
||||
// 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<float>(tile_col * kTileDisplaySize);
|
||||
float tile_y = static_cast<float>(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_.set_scrolling(ImVec2(-scroll_x, -scroll_y));
|
||||
}
|
||||
|
||||
void OverworldEditor::DrawOverworldProperties() {
|
||||
@@ -2610,4 +2636,13 @@ absl::Status OverworldEditor::UpdateROMVersionMarkers(int target_version) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
void OverworldEditor::UpdateBlocksetSelectorState() {
|
||||
if (!blockset_selector_) {
|
||||
return;
|
||||
}
|
||||
|
||||
blockset_selector_->SetTileCount(zelda3::kNumTile16Individual);
|
||||
blockset_selector_->SetSelectedTile(current_tile16_);
|
||||
}
|
||||
|
||||
} // namespace yaze::editor
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "app/gfx/snes_palette.h"
|
||||
#include "app/gfx/tilemap.h"
|
||||
#include "app/gui/canvas.h"
|
||||
#include "app/gui/widgets/tile_selector_widget.h"
|
||||
#include "app/gui/input.h"
|
||||
#include "app/rom.h"
|
||||
#include "app/zelda3/overworld/overworld.h"
|
||||
@@ -160,6 +161,7 @@ class OverworldEditor : public Editor, public gfx::GfxContext {
|
||||
absl::Status DrawTile16Selector();
|
||||
void DrawTile8Selector();
|
||||
absl::Status DrawAreaGraphics();
|
||||
void UpdateBlocksetSelectorState();
|
||||
|
||||
absl::Status LoadSpriteGraphics();
|
||||
|
||||
@@ -182,7 +184,13 @@ class OverworldEditor : public Editor, public gfx::GfxContext {
|
||||
|
||||
void DrawOverworldProperties();
|
||||
void HandleMapInteraction();
|
||||
void SetupOverworldCanvasContextMenu();
|
||||
// SetupOverworldCanvasContextMenu removed (Phase 3B) - now handled by MapPropertiesSystem
|
||||
|
||||
// Canvas pan/zoom helpers (Overworld Refactoring)
|
||||
void HandleOverworldPan();
|
||||
void HandleOverworldZoom();
|
||||
void ResetOverworldView();
|
||||
void CenterOverworldView();
|
||||
|
||||
/**
|
||||
* @brief Scroll the blockset canvas to show the current selected tile16
|
||||
@@ -332,6 +340,7 @@ class OverworldEditor : public Editor, public gfx::GfxContext {
|
||||
gui::CanvasGridSize::k32x32};
|
||||
gui::Canvas blockset_canvas_{"OwBlockset", kBlocksetCanvasSize,
|
||||
gui::CanvasGridSize::k32x32};
|
||||
std::unique_ptr<gui::TileSelectorWidget> blockset_selector_;
|
||||
gui::Canvas graphics_bin_canvas_{"GraphicsBin", kGraphicsBinCanvasSize,
|
||||
gui::CanvasGridSize::k16x16};
|
||||
gui::Canvas properties_canvas_;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "app/gui/canvas_utils.h"
|
||||
#include "app/gui/color.h"
|
||||
#include "app/gui/style.h"
|
||||
#include "app/gui/canvas/canvas_automation_api.h"
|
||||
#include "imgui/imgui.h"
|
||||
#include "imgui_memory_editor.h"
|
||||
#include "util/log.h"
|
||||
@@ -19,6 +20,40 @@ namespace yaze::gui {
|
||||
|
||||
using core::Renderer;
|
||||
|
||||
// Define constructors and destructor in .cc to avoid incomplete type issues with unique_ptr
|
||||
Canvas::Canvas() { InitializeDefaults(); }
|
||||
|
||||
Canvas::Canvas(const std::string& id)
|
||||
: canvas_id_(id), context_id_(id + "Context") {
|
||||
InitializeDefaults();
|
||||
}
|
||||
|
||||
Canvas::Canvas(const std::string& id, ImVec2 canvas_size)
|
||||
: canvas_id_(id), context_id_(id + "Context") {
|
||||
InitializeDefaults();
|
||||
config_.canvas_size = canvas_size;
|
||||
config_.custom_canvas_size = true;
|
||||
}
|
||||
|
||||
Canvas::Canvas(const std::string& id, ImVec2 canvas_size, CanvasGridSize grid_size)
|
||||
: canvas_id_(id), context_id_(id + "Context") {
|
||||
InitializeDefaults();
|
||||
config_.canvas_size = canvas_size;
|
||||
config_.custom_canvas_size = true;
|
||||
SetGridSize(grid_size);
|
||||
}
|
||||
|
||||
Canvas::Canvas(const std::string& id, ImVec2 canvas_size, CanvasGridSize grid_size, float global_scale)
|
||||
: canvas_id_(id), context_id_(id + "Context") {
|
||||
InitializeDefaults();
|
||||
config_.canvas_size = canvas_size;
|
||||
config_.custom_canvas_size = true;
|
||||
config_.global_scale = global_scale;
|
||||
SetGridSize(grid_size);
|
||||
}
|
||||
|
||||
Canvas::~Canvas() = default;
|
||||
|
||||
using ImGui::BeginMenu;
|
||||
using ImGui::EndMenu;
|
||||
using ImGui::GetContentRegionAvail;
|
||||
@@ -1778,4 +1813,12 @@ gfx::BppFormat Canvas::GetCurrentBppFormat() const {
|
||||
bitmap_->vector(), bitmap_->width(), bitmap_->height());
|
||||
}
|
||||
|
||||
// Phase 4A: Canvas Automation API
|
||||
CanvasAutomationAPI* Canvas::GetAutomationAPI() {
|
||||
if (!automation_api_) {
|
||||
automation_api_ = std::make_unique<CanvasAutomationAPI>(this);
|
||||
}
|
||||
return automation_api_.get();
|
||||
}
|
||||
|
||||
} // namespace yaze::gui
|
||||
|
||||
@@ -30,6 +30,9 @@ namespace yaze {
|
||||
*/
|
||||
namespace gui {
|
||||
|
||||
// Forward declaration (full include would cause circular dependency)
|
||||
class CanvasAutomationAPI;
|
||||
|
||||
using gfx::Bitmap;
|
||||
using gfx::BitmapTable;
|
||||
|
||||
@@ -50,36 +53,13 @@ enum class CanvasGridSize { k8x8, k16x16, k32x32, k64x64 };
|
||||
*/
|
||||
class Canvas {
|
||||
public:
|
||||
Canvas() = default;
|
||||
Canvas();
|
||||
~Canvas();
|
||||
|
||||
explicit Canvas(const std::string& id)
|
||||
: canvas_id_(id), context_id_(id + "Context") {
|
||||
InitializeDefaults();
|
||||
}
|
||||
|
||||
explicit Canvas(const std::string& id, ImVec2 canvas_size)
|
||||
: canvas_id_(id), context_id_(id + "Context") {
|
||||
InitializeDefaults();
|
||||
config_.canvas_size = canvas_size;
|
||||
config_.custom_canvas_size = true;
|
||||
}
|
||||
|
||||
explicit Canvas(const std::string& id, ImVec2 canvas_size, CanvasGridSize grid_size)
|
||||
: canvas_id_(id), context_id_(id + "Context") {
|
||||
InitializeDefaults();
|
||||
config_.canvas_size = canvas_size;
|
||||
config_.custom_canvas_size = true;
|
||||
SetGridSize(grid_size);
|
||||
}
|
||||
|
||||
explicit Canvas(const std::string& id, ImVec2 canvas_size, CanvasGridSize grid_size, float global_scale)
|
||||
: canvas_id_(id), context_id_(id + "Context") {
|
||||
InitializeDefaults();
|
||||
config_.canvas_size = canvas_size;
|
||||
config_.custom_canvas_size = true;
|
||||
config_.global_scale = global_scale;
|
||||
SetGridSize(grid_size);
|
||||
}
|
||||
explicit Canvas(const std::string& id);
|
||||
explicit Canvas(const std::string& id, ImVec2 canvas_size);
|
||||
explicit Canvas(const std::string& id, ImVec2 canvas_size, CanvasGridSize grid_size);
|
||||
explicit Canvas(const std::string& id, ImVec2 canvas_size, CanvasGridSize grid_size, float global_scale);
|
||||
|
||||
void SetGridSize(CanvasGridSize grid_size) {
|
||||
switch (grid_size) {
|
||||
@@ -100,6 +80,14 @@ class Canvas {
|
||||
|
||||
// Legacy compatibility
|
||||
void SetCanvasGridSize(CanvasGridSize grid_size) { SetGridSize(grid_size); }
|
||||
|
||||
CanvasGridSize grid_size() const {
|
||||
if (config_.grid_step == 8.0f) return CanvasGridSize::k8x8;
|
||||
if (config_.grid_step == 16.0f) return CanvasGridSize::k16x16;
|
||||
if (config_.grid_step == 32.0f) return CanvasGridSize::k32x32;
|
||||
if (config_.grid_step == 64.0f) return CanvasGridSize::k64x64;
|
||||
return CanvasGridSize::k16x16; // Default
|
||||
}
|
||||
|
||||
void UpdateColorPainter(gfx::Bitmap &bitmap, const ImVec4 &color,
|
||||
const std::function<void()> &event, int tile_size,
|
||||
@@ -235,6 +223,9 @@ class Canvas {
|
||||
canvas::CanvasInteractionHandler& GetInteractionHandler() { return interaction_handler_; }
|
||||
const canvas::CanvasInteractionHandler& GetInteractionHandler() const { return interaction_handler_; }
|
||||
|
||||
// Automation API access (Phase 4A)
|
||||
CanvasAutomationAPI* GetAutomationAPI();
|
||||
|
||||
// Initialization and cleanup
|
||||
void InitializeDefaults();
|
||||
void Cleanup();
|
||||
@@ -401,6 +392,7 @@ class Canvas {
|
||||
auto set_highlight_tile_id(int i) { highlight_tile_id = i; }
|
||||
|
||||
auto mutable_selected_tiles() { return &selected_tiles_; }
|
||||
auto mutable_selected_points() { return &selected_points_; }
|
||||
auto selected_points() const { return selected_points_; }
|
||||
|
||||
auto hover_mouse_pos() const { return mouse_pos_in_canvas_; }
|
||||
@@ -414,6 +406,9 @@ class Canvas {
|
||||
CanvasSelection selection_;
|
||||
std::unique_ptr<PaletteWidget> palette_editor_;
|
||||
|
||||
// Automation API (lazy-initialized on first access)
|
||||
std::unique_ptr<CanvasAutomationAPI> automation_api_;
|
||||
|
||||
// Core canvas state
|
||||
bool is_hovered_ = false;
|
||||
bool refresh_graphics_ = false;
|
||||
|
||||
303
src/app/gui/canvas/canvas_automation_api.cc
Normal file
303
src/app/gui/canvas/canvas_automation_api.cc
Normal file
@@ -0,0 +1,303 @@
|
||||
#include "app/gui/canvas/canvas_automation_api.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "app/gui/canvas.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace gui {
|
||||
|
||||
CanvasAutomationAPI::CanvasAutomationAPI(Canvas* canvas) : canvas_(canvas) {}
|
||||
|
||||
// ============================================================================
|
||||
// Tile Operations
|
||||
// ============================================================================
|
||||
|
||||
bool CanvasAutomationAPI::SetTileAt(int x, int y, int tile_id) {
|
||||
if (!IsInBounds(x, y)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tile_paint_callback_) {
|
||||
return tile_paint_callback_(x, y, tile_id);
|
||||
}
|
||||
|
||||
// Default behavior: add to canvas points for drawing
|
||||
// Note: Actual tile painting depends on the editor's canvas integration
|
||||
ImVec2 canvas_pos = TileToCanvas(x, y);
|
||||
canvas_->mutable_points()->push_back(canvas_pos);
|
||||
return true;
|
||||
}
|
||||
|
||||
int CanvasAutomationAPI::GetTileAt(int x, int y) const {
|
||||
if (!IsInBounds(x, y)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (tile_query_callback_) {
|
||||
return tile_query_callback_(x, y);
|
||||
}
|
||||
|
||||
// Default: return -1 (requires callback for actual implementation)
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool CanvasAutomationAPI::SetTiles(
|
||||
const std::vector<std::tuple<int, int, int>>& tiles) {
|
||||
bool all_success = true;
|
||||
for (const auto& [x, y, tile_id] : tiles) {
|
||||
if (!SetTileAt(x, y, tile_id)) {
|
||||
all_success = false;
|
||||
}
|
||||
}
|
||||
return all_success;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Selection Operations
|
||||
// ============================================================================
|
||||
|
||||
void CanvasAutomationAPI::SelectTile(int x, int y) {
|
||||
if (!IsInBounds(x, y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImVec2 canvas_pos = TileToCanvas(x, y);
|
||||
canvas_->mutable_selected_points()->clear();
|
||||
canvas_->mutable_selected_points()->push_back(canvas_pos);
|
||||
canvas_->mutable_selected_points()->push_back(canvas_pos);
|
||||
}
|
||||
|
||||
void CanvasAutomationAPI::SelectTileRect(int x1, int y1, int x2, int y2) {
|
||||
// Ensure x1 <= x2 and y1 <= y2
|
||||
if (x1 > x2) std::swap(x1, x2);
|
||||
if (y1 > y2) std::swap(y1, y2);
|
||||
|
||||
if (!IsInBounds(x1, y1) || !IsInBounds(x2, y2)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImVec2 start = TileToCanvas(x1, y1);
|
||||
ImVec2 end = TileToCanvas(x2, y2);
|
||||
|
||||
canvas_->mutable_selected_points()->clear();
|
||||
canvas_->mutable_selected_points()->push_back(start);
|
||||
canvas_->mutable_selected_points()->push_back(end);
|
||||
}
|
||||
|
||||
CanvasAutomationAPI::SelectionState CanvasAutomationAPI::GetSelection() const {
|
||||
SelectionState state;
|
||||
|
||||
const auto& selected_points = canvas_->selected_points();
|
||||
if (selected_points.size() >= 2) {
|
||||
state.has_selection = true;
|
||||
state.selection_start = selected_points[0];
|
||||
state.selection_end = selected_points[1];
|
||||
|
||||
// Convert canvas positions back to tile coordinates
|
||||
ImVec2 tile_start = CanvasToTile(state.selection_start);
|
||||
ImVec2 tile_end = CanvasToTile(state.selection_end);
|
||||
|
||||
// Ensure proper ordering
|
||||
int min_x = std::min(static_cast<int>(tile_start.x),
|
||||
static_cast<int>(tile_end.x));
|
||||
int max_x = std::max(static_cast<int>(tile_start.x),
|
||||
static_cast<int>(tile_end.x));
|
||||
int min_y = std::min(static_cast<int>(tile_start.y),
|
||||
static_cast<int>(tile_end.y));
|
||||
int max_y = std::max(static_cast<int>(tile_start.y),
|
||||
static_cast<int>(tile_end.y));
|
||||
|
||||
// Generate all tiles in selection rectangle
|
||||
for (int y = min_y; y <= max_y; ++y) {
|
||||
for (int x = min_x; x <= max_x; ++x) {
|
||||
state.selected_tiles.push_back(ImVec2(static_cast<float>(x),
|
||||
static_cast<float>(y)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
void CanvasAutomationAPI::ClearSelection() {
|
||||
canvas_->mutable_selected_points()->clear();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// View Operations
|
||||
// ============================================================================
|
||||
|
||||
void CanvasAutomationAPI::ScrollToTile(int x, int y, bool center) {
|
||||
if (!IsInBounds(x, y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (center) {
|
||||
CenterOn(x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if tile is already visible
|
||||
if (IsTileVisible(x, y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Scroll to make tile visible
|
||||
ImVec2 tile_canvas_pos = TileToCanvas(x, y);
|
||||
|
||||
// Get current scroll and canvas size
|
||||
ImVec2 current_scroll = canvas_->scrolling();
|
||||
ImVec2 canvas_size = canvas_->canvas_size();
|
||||
|
||||
// Calculate new scroll to make tile visible at top-left
|
||||
float new_scroll_x = -tile_canvas_pos.x;
|
||||
float new_scroll_y = -tile_canvas_pos.y;
|
||||
|
||||
canvas_->set_scrolling(ImVec2(new_scroll_x, new_scroll_y));
|
||||
}
|
||||
|
||||
void CanvasAutomationAPI::CenterOn(int x, int y) {
|
||||
if (!IsInBounds(x, y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImVec2 tile_canvas_pos = TileToCanvas(x, y);
|
||||
ImVec2 canvas_size = canvas_->canvas_size();
|
||||
|
||||
// Center the tile in the canvas view
|
||||
float new_scroll_x = -(tile_canvas_pos.x - canvas_size.x / 2.0f);
|
||||
float new_scroll_y = -(tile_canvas_pos.y - canvas_size.y / 2.0f);
|
||||
|
||||
canvas_->set_scrolling(ImVec2(new_scroll_x, new_scroll_y));
|
||||
}
|
||||
|
||||
void CanvasAutomationAPI::SetZoom(float zoom) {
|
||||
// Clamp zoom to reasonable range
|
||||
zoom = std::max(0.25f, std::min(zoom, 4.0f));
|
||||
canvas_->set_global_scale(zoom);
|
||||
}
|
||||
|
||||
float CanvasAutomationAPI::GetZoom() const {
|
||||
return canvas_->global_scale();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Query Operations
|
||||
// ============================================================================
|
||||
|
||||
CanvasAutomationAPI::Dimensions CanvasAutomationAPI::GetDimensions() const {
|
||||
Dimensions dims;
|
||||
|
||||
// Get canvas size in pixels
|
||||
ImVec2 canvas_size = canvas_->canvas_size();
|
||||
float scale = canvas_->global_scale();
|
||||
|
||||
// Determine tile size from canvas grid size
|
||||
int tile_size = 16; // Default
|
||||
switch (canvas_->grid_size()) {
|
||||
case CanvasGridSize::k8x8:
|
||||
tile_size = 8;
|
||||
break;
|
||||
case CanvasGridSize::k16x16:
|
||||
tile_size = 16;
|
||||
break;
|
||||
case CanvasGridSize::k32x32:
|
||||
tile_size = 32;
|
||||
break;
|
||||
case CanvasGridSize::k64x64:
|
||||
tile_size = 64;
|
||||
break;
|
||||
}
|
||||
|
||||
dims.tile_size = tile_size;
|
||||
dims.width_tiles = static_cast<int>(canvas_size.x / (tile_size * scale));
|
||||
dims.height_tiles = static_cast<int>(canvas_size.y / (tile_size * scale));
|
||||
|
||||
return dims;
|
||||
}
|
||||
|
||||
CanvasAutomationAPI::VisibleRegion CanvasAutomationAPI::GetVisibleRegion() const {
|
||||
VisibleRegion region;
|
||||
|
||||
ImVec2 scroll = canvas_->scrolling();
|
||||
ImVec2 canvas_size = canvas_->canvas_size();
|
||||
float scale = canvas_->global_scale();
|
||||
int tile_size = GetDimensions().tile_size;
|
||||
|
||||
// Top-left corner of visible region
|
||||
ImVec2 top_left = CanvasToTile(ImVec2(-scroll.x, -scroll.y));
|
||||
|
||||
// Bottom-right corner of visible region
|
||||
ImVec2 bottom_right = CanvasToTile(ImVec2(-scroll.x + canvas_size.x,
|
||||
-scroll.y + canvas_size.y));
|
||||
|
||||
region.min_x = std::max(0, static_cast<int>(top_left.x));
|
||||
region.min_y = std::max(0, static_cast<int>(top_left.y));
|
||||
|
||||
Dimensions dims = GetDimensions();
|
||||
region.max_x = std::min(dims.width_tiles - 1, static_cast<int>(bottom_right.x));
|
||||
region.max_y = std::min(dims.height_tiles - 1, static_cast<int>(bottom_right.y));
|
||||
|
||||
return region;
|
||||
}
|
||||
|
||||
bool CanvasAutomationAPI::IsTileVisible(int x, int y) const {
|
||||
if (!IsInBounds(x, y)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
VisibleRegion region = GetVisibleRegion();
|
||||
return x >= region.min_x && x <= region.max_x &&
|
||||
y >= region.min_y && y <= region.max_y;
|
||||
}
|
||||
|
||||
bool CanvasAutomationAPI::IsInBounds(int x, int y) const {
|
||||
if (x < 0 || y < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Dimensions dims = GetDimensions();
|
||||
return x < dims.width_tiles && y < dims.height_tiles;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Coordinate Conversion
|
||||
// ============================================================================
|
||||
|
||||
ImVec2 CanvasAutomationAPI::TileToCanvas(int x, int y) const {
|
||||
int tile_size = GetDimensions().tile_size;
|
||||
float scale = canvas_->global_scale();
|
||||
|
||||
float canvas_x = x * tile_size * scale;
|
||||
float canvas_y = y * tile_size * scale;
|
||||
|
||||
return ImVec2(canvas_x, canvas_y);
|
||||
}
|
||||
|
||||
ImVec2 CanvasAutomationAPI::CanvasToTile(ImVec2 canvas_pos) const {
|
||||
int tile_size = GetDimensions().tile_size;
|
||||
float scale = canvas_->global_scale();
|
||||
|
||||
float tile_x = canvas_pos.x / (tile_size * scale);
|
||||
float tile_y = canvas_pos.y / (tile_size * scale);
|
||||
|
||||
return ImVec2(std::floor(tile_x), std::floor(tile_y));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Callback Registration
|
||||
// ============================================================================
|
||||
|
||||
void CanvasAutomationAPI::SetTilePaintCallback(TilePaintCallback callback) {
|
||||
tile_paint_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
void CanvasAutomationAPI::SetTileQueryCallback(TileQueryCallback callback) {
|
||||
tile_query_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
} // namespace gui
|
||||
} // namespace yaze
|
||||
|
||||
224
src/app/gui/canvas/canvas_automation_api.h
Normal file
224
src/app/gui/canvas/canvas_automation_api.h
Normal file
@@ -0,0 +1,224 @@
|
||||
#ifndef YAZE_APP_GUI_CANVAS_CANVAS_AUTOMATION_API_H
|
||||
#define YAZE_APP_GUI_CANVAS_CANVAS_AUTOMATION_API_H
|
||||
|
||||
#include <functional>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace gui {
|
||||
|
||||
// Forward declaration
|
||||
class Canvas;
|
||||
|
||||
/**
|
||||
* @brief Programmatic interface for controlling canvas operations.
|
||||
*
|
||||
* Enables z3ed CLI, AI agents, GUI automation, and remote control via gRPC.
|
||||
* All operations work with logical tile coordinates, independent of zoom/scroll.
|
||||
*/
|
||||
class CanvasAutomationAPI {
|
||||
public:
|
||||
/**
|
||||
* @brief Selection state returned by GetSelection().
|
||||
*/
|
||||
struct SelectionState {
|
||||
bool has_selection = false;
|
||||
std::vector<ImVec2> selected_tiles;
|
||||
ImVec2 selection_start = {0, 0};
|
||||
ImVec2 selection_end = {0, 0};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Canvas dimensions in logical tile units.
|
||||
*/
|
||||
struct Dimensions {
|
||||
int width_tiles = 0;
|
||||
int height_tiles = 0;
|
||||
int tile_size = 16;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Visible region in logical tile coordinates.
|
||||
*/
|
||||
struct VisibleRegion {
|
||||
int min_x = 0;
|
||||
int min_y = 0;
|
||||
int max_x = 0;
|
||||
int max_y = 0;
|
||||
};
|
||||
|
||||
explicit CanvasAutomationAPI(Canvas* canvas);
|
||||
|
||||
// ============================================================================
|
||||
// Tile Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Paint a single tile at logical coordinates.
|
||||
* @param x Logical X coordinate (tile units)
|
||||
* @param y Logical Y coordinate (tile units)
|
||||
* @param tile_id Tile ID to paint
|
||||
* @return true if successful, false if out of bounds
|
||||
*/
|
||||
bool SetTileAt(int x, int y, int tile_id);
|
||||
|
||||
/**
|
||||
* @brief Query tile ID at logical coordinates.
|
||||
* @param x Logical X coordinate
|
||||
* @param y Logical Y coordinate
|
||||
* @return Tile ID at position, or -1 if out of bounds
|
||||
*/
|
||||
int GetTileAt(int x, int y) const;
|
||||
|
||||
/**
|
||||
* @brief Paint multiple tiles in a batch operation.
|
||||
* @param tiles Vector of (x, y, tile_id) tuples
|
||||
* @return true if all tiles painted successfully
|
||||
*/
|
||||
bool SetTiles(const std::vector<std::tuple<int, int, int>>& tiles);
|
||||
|
||||
// ============================================================================
|
||||
// Selection Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Select a single tile.
|
||||
* @param x Logical X coordinate
|
||||
* @param y Logical Y coordinate
|
||||
*/
|
||||
void SelectTile(int x, int y);
|
||||
|
||||
/**
|
||||
* @brief Select a rectangular region of tiles.
|
||||
* @param x1 Top-left X coordinate (logical)
|
||||
* @param y1 Top-left Y coordinate (logical)
|
||||
* @param x2 Bottom-right X coordinate (logical)
|
||||
* @param y2 Bottom-right Y coordinate (logical)
|
||||
*/
|
||||
void SelectTileRect(int x1, int y1, int x2, int y2);
|
||||
|
||||
/**
|
||||
* @brief Query current selection state.
|
||||
* @return Selection state with all selected tiles
|
||||
*/
|
||||
SelectionState GetSelection() const;
|
||||
|
||||
/**
|
||||
* @brief Clear current selection.
|
||||
*/
|
||||
void ClearSelection();
|
||||
|
||||
// ============================================================================
|
||||
// View Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Scroll canvas to make tile visible.
|
||||
* @param x Logical X coordinate
|
||||
* @param y Logical Y coordinate
|
||||
* @param center If true, center the tile; otherwise just ensure visibility
|
||||
*/
|
||||
void ScrollToTile(int x, int y, bool center = false);
|
||||
|
||||
/**
|
||||
* @brief Center canvas view on a specific tile.
|
||||
* @param x Logical X coordinate
|
||||
* @param y Logical Y coordinate
|
||||
*/
|
||||
void CenterOn(int x, int y);
|
||||
|
||||
/**
|
||||
* @brief Set canvas zoom level.
|
||||
* @param zoom Zoom factor (0.25 to 4.0, default 1.0)
|
||||
*/
|
||||
void SetZoom(float zoom);
|
||||
|
||||
/**
|
||||
* @brief Get current zoom level.
|
||||
* @return Current zoom factor
|
||||
*/
|
||||
float GetZoom() const;
|
||||
|
||||
// ============================================================================
|
||||
// Query Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Get canvas dimensions in logical tile units.
|
||||
* @return Canvas dimensions
|
||||
*/
|
||||
Dimensions GetDimensions() const;
|
||||
|
||||
/**
|
||||
* @brief Get currently visible tile region.
|
||||
* @return Visible region bounds in logical coordinates
|
||||
*/
|
||||
VisibleRegion GetVisibleRegion() const;
|
||||
|
||||
/**
|
||||
* @brief Check if a tile is currently visible.
|
||||
* @param x Logical X coordinate
|
||||
* @param y Logical Y coordinate
|
||||
* @return true if tile is visible on canvas
|
||||
*/
|
||||
bool IsTileVisible(int x, int y) const;
|
||||
|
||||
/**
|
||||
* @brief Check if coordinates are within canvas bounds.
|
||||
* @param x Logical X coordinate
|
||||
* @param y Logical Y coordinate
|
||||
* @return true if coordinates are valid
|
||||
*/
|
||||
bool IsInBounds(int x, int y) const;
|
||||
|
||||
// ============================================================================
|
||||
// Coordinate Conversion
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Convert logical tile coordinates to canvas pixel coordinates.
|
||||
* @param x Logical X coordinate
|
||||
* @param y Logical Y coordinate
|
||||
* @return Canvas pixel position
|
||||
*/
|
||||
ImVec2 TileToCanvas(int x, int y) const;
|
||||
|
||||
/**
|
||||
* @brief Convert canvas pixel coordinates to logical tile coordinates.
|
||||
* @param canvas_pos Canvas pixel position
|
||||
* @return Logical tile position
|
||||
*/
|
||||
ImVec2 CanvasToTile(ImVec2 canvas_pos) const;
|
||||
|
||||
// ============================================================================
|
||||
// Callback Registration (for external integrations)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Set callback for tile painting operations.
|
||||
* Allows external systems (CLI, AI agents) to implement custom tile logic.
|
||||
*/
|
||||
using TilePaintCallback = std::function<bool(int x, int y, int tile_id)>;
|
||||
void SetTilePaintCallback(TilePaintCallback callback);
|
||||
|
||||
/**
|
||||
* @brief Set callback for tile querying operations.
|
||||
* Allows external systems to provide tile data.
|
||||
*/
|
||||
using TileQueryCallback = std::function<int(int x, int y)>;
|
||||
void SetTileQueryCallback(TileQueryCallback callback);
|
||||
|
||||
private:
|
||||
Canvas* canvas_;
|
||||
TilePaintCallback tile_paint_callback_;
|
||||
TileQueryCallback tile_query_callback_;
|
||||
};
|
||||
|
||||
} // namespace gui
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_GUI_CANVAS_CANVAS_AUTOMATION_API_H
|
||||
|
||||
@@ -23,6 +23,8 @@ set(
|
||||
app/gui/canvas/canvas_usage_tracker.cc
|
||||
app/gui/canvas/canvas_performance_integration.cc
|
||||
app/gui/canvas/canvas_interaction_handler.cc
|
||||
app/gui/canvas/canvas_automation_api.cc
|
||||
app/gui/widgets/tile_selector_widget.cc
|
||||
)
|
||||
|
||||
# ==============================================================================
|
||||
|
||||
@@ -422,39 +422,6 @@ void DrawTable(Table& params) {
|
||||
}
|
||||
}
|
||||
|
||||
void DrawMenu(Menu& menu) {
|
||||
for (const auto& each_menu : menu) {
|
||||
if (ImGui::BeginMenu(each_menu.name.c_str())) {
|
||||
for (const auto& each_item : each_menu.subitems) {
|
||||
if (!each_item.subitems.empty()) {
|
||||
if (ImGui::BeginMenu(each_item.name.c_str())) {
|
||||
for (const auto& each_subitem : each_item.subitems) {
|
||||
if (each_subitem.name == kSeparator) {
|
||||
ImGui::Separator();
|
||||
} else if (ImGui::MenuItem(each_subitem.name.c_str(),
|
||||
each_subitem.shortcut.c_str())) {
|
||||
if (each_subitem.callback)
|
||||
each_subitem.callback();
|
||||
}
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
} else {
|
||||
if (each_item.name == kSeparator) {
|
||||
ImGui::Separator();
|
||||
} else if (ImGui::MenuItem(each_item.name.c_str(),
|
||||
each_item.shortcut.c_str(),
|
||||
each_item.enabled_condition())) {
|
||||
if (each_item.callback)
|
||||
each_item.callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool OpenUrl(const std::string& url) {
|
||||
// if iOS
|
||||
#ifdef __APPLE__
|
||||
@@ -477,20 +444,6 @@ bool OpenUrl(const std::string& url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void RenderLayout(const Layout& layout) {
|
||||
for (const auto& element : layout.elements) {
|
||||
std::visit(overloaded{[](const Text& text) {
|
||||
ImGui::Text("%s", text.content.c_str());
|
||||
},
|
||||
[](const Button& button) {
|
||||
if (ImGui::Button(button.label.c_str())) {
|
||||
button.callback();
|
||||
}
|
||||
}},
|
||||
element);
|
||||
}
|
||||
}
|
||||
|
||||
void MemoryEditorPopup(const std::string& label, std::span<uint8_t> memory) {
|
||||
static bool open = false;
|
||||
static MemoryEditor editor;
|
||||
|
||||
@@ -77,63 +77,8 @@ struct Table {
|
||||
|
||||
void AddTableColumn(Table &table, const std::string &label, GuiElement element);
|
||||
|
||||
void DrawTable(Table ¶ms);
|
||||
|
||||
static std::function<bool()> kDefaultEnabledCondition = []() { return false; };
|
||||
|
||||
struct MenuItem {
|
||||
std::string name;
|
||||
std::string shortcut;
|
||||
std::function<void()> callback;
|
||||
std::function<bool()> enabled_condition = kDefaultEnabledCondition;
|
||||
std::vector<MenuItem> subitems;
|
||||
|
||||
// Default constructor
|
||||
MenuItem() = default;
|
||||
|
||||
// Constructor for basic menu items
|
||||
MenuItem(const std::string& name, const std::string& shortcut,
|
||||
std::function<void()> callback)
|
||||
: name(name), shortcut(shortcut), callback(callback) {}
|
||||
|
||||
// Constructor for menu items with enabled condition
|
||||
MenuItem(const std::string& name, const std::string& shortcut,
|
||||
std::function<void()> callback, std::function<bool()> enabled_condition)
|
||||
: name(name), shortcut(shortcut), callback(callback),
|
||||
enabled_condition(enabled_condition) {}
|
||||
|
||||
// Constructor for menu items with subitems
|
||||
MenuItem(const std::string& name, const std::string& shortcut,
|
||||
std::function<void()> callback, std::function<bool()> enabled_condition,
|
||||
std::vector<MenuItem> subitems)
|
||||
: name(name), shortcut(shortcut), callback(callback),
|
||||
enabled_condition(enabled_condition), subitems(std::move(subitems)) {}
|
||||
};
|
||||
using Menu = std::vector<MenuItem>;
|
||||
|
||||
void DrawMenu(Menu ¶ms);
|
||||
|
||||
static Menu kMainMenu;
|
||||
|
||||
const std::string kSeparator = "-";
|
||||
|
||||
IMGUI_API bool OpenUrl(const std::string &url);
|
||||
|
||||
struct Text {
|
||||
std::string content;
|
||||
};
|
||||
|
||||
struct Button {
|
||||
std::string label;
|
||||
std::function<void()> callback;
|
||||
};
|
||||
|
||||
struct Layout {
|
||||
std::vector<std::variant<Text, Button>> elements;
|
||||
};
|
||||
|
||||
void RenderLayout(const Layout &layout);
|
||||
|
||||
void MemoryEditorPopup(const std::string &label, std::span<uint8_t> memory);
|
||||
|
||||
} // namespace gui
|
||||
|
||||
190
src/app/gui/widgets/tile_selector_widget.cc
Normal file
190
src/app/gui/widgets/tile_selector_widget.cc
Normal file
@@ -0,0 +1,190 @@
|
||||
#include "app/gui/widgets/tile_selector_widget.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace yaze::gui {
|
||||
|
||||
TileSelectorWidget::TileSelectorWidget(std::string widget_id)
|
||||
: config_(), total_tiles_(config_.total_tiles), widget_id_(std::move(widget_id)) {}
|
||||
|
||||
TileSelectorWidget::TileSelectorWidget(std::string widget_id, Config config)
|
||||
: config_(config), total_tiles_(config.total_tiles), widget_id_(std::move(widget_id)) {}
|
||||
|
||||
void TileSelectorWidget::AttachCanvas(Canvas* canvas) { canvas_ = canvas; }
|
||||
|
||||
void TileSelectorWidget::SetTileCount(int total_tiles) {
|
||||
total_tiles_ = std::max(total_tiles, 0);
|
||||
if (!IsValidTileId(selected_tile_id_)) {
|
||||
selected_tile_id_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void TileSelectorWidget::SetSelectedTile(int tile_id) {
|
||||
if (IsValidTileId(tile_id)) {
|
||||
selected_tile_id_ = tile_id;
|
||||
}
|
||||
}
|
||||
|
||||
TileSelectorWidget::RenderResult TileSelectorWidget::Render(gfx::Bitmap& atlas,
|
||||
bool atlas_ready) {
|
||||
RenderResult result;
|
||||
|
||||
if (!canvas_) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const int tile_display_size =
|
||||
static_cast<int>(config_.tile_size * config_.display_scale);
|
||||
|
||||
// Calculate total content size for ImGui child window scrolling
|
||||
const int num_rows = (total_tiles_ + config_.tiles_per_row - 1) / config_.tiles_per_row;
|
||||
const ImVec2 content_size(
|
||||
config_.tiles_per_row * tile_display_size + config_.draw_offset.x * 2,
|
||||
num_rows * tile_display_size + config_.draw_offset.y * 2
|
||||
);
|
||||
|
||||
// Set content size for ImGui child window (must be called before DrawBackground)
|
||||
ImGui::SetCursorPos(ImVec2(0, 0));
|
||||
ImGui::Dummy(content_size);
|
||||
ImGui::SetCursorPos(ImVec2(0, 0));
|
||||
|
||||
// Handle pending scroll (deferred from ScrollToTile call outside render context)
|
||||
if (pending_scroll_tile_id_ >= 0) {
|
||||
if (IsValidTileId(pending_scroll_tile_id_)) {
|
||||
const ImVec2 target = TileOrigin(pending_scroll_tile_id_);
|
||||
if (pending_scroll_use_imgui_) {
|
||||
const ImVec2 window_size = ImGui::GetWindowSize();
|
||||
float scroll_x = target.x - (window_size.x / 2.0f) + (tile_display_size / 2.0f);
|
||||
float scroll_y = target.y - (window_size.y / 2.0f) + (tile_display_size / 2.0f);
|
||||
scroll_x = std::max(0.0f, scroll_x);
|
||||
scroll_y = std::max(0.0f, scroll_y);
|
||||
ImGui::SetScrollX(scroll_x);
|
||||
ImGui::SetScrollY(scroll_y);
|
||||
}
|
||||
}
|
||||
pending_scroll_tile_id_ = -1; // Clear pending scroll
|
||||
}
|
||||
|
||||
canvas_->DrawBackground();
|
||||
canvas_->DrawContextMenu();
|
||||
|
||||
if (atlas_ready && atlas.is_active()) {
|
||||
canvas_->DrawBitmap(atlas, static_cast<int>(config_.draw_offset.x),
|
||||
static_cast<int>(config_.draw_offset.y),
|
||||
config_.display_scale);
|
||||
|
||||
result = HandleInteraction(tile_display_size);
|
||||
|
||||
if (config_.show_tile_ids) {
|
||||
DrawTileIdLabels(tile_display_size);
|
||||
}
|
||||
|
||||
DrawHighlight(tile_display_size);
|
||||
}
|
||||
|
||||
canvas_->DrawGrid();
|
||||
canvas_->DrawOverlay();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
TileSelectorWidget::RenderResult TileSelectorWidget::HandleInteraction(
|
||||
int tile_display_size) {
|
||||
RenderResult result;
|
||||
|
||||
if (!ImGui::IsItemHovered()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const bool clicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
|
||||
const bool double_clicked =
|
||||
ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
|
||||
|
||||
if (clicked || double_clicked) {
|
||||
const int hovered_tile = ResolveTileAtCursor(tile_display_size);
|
||||
if (IsValidTileId(hovered_tile)) {
|
||||
result.tile_clicked = clicked;
|
||||
result.tile_double_clicked = double_clicked;
|
||||
if (hovered_tile != selected_tile_id_) {
|
||||
selected_tile_id_ = hovered_tile;
|
||||
result.selection_changed = true;
|
||||
}
|
||||
result.selected_tile = selected_tile_id_;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int TileSelectorWidget::ResolveTileAtCursor(int tile_display_size) const {
|
||||
if (!canvas_) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const ImVec2 screen_pos = ImGui::GetIO().MousePos;
|
||||
const ImVec2 origin = canvas_->zero_point();
|
||||
const ImVec2 scroll = canvas_->scrolling();
|
||||
|
||||
// Convert screen position to canvas content position (accounting for scroll)
|
||||
ImVec2 local = ImVec2(screen_pos.x - origin.x - config_.draw_offset.x - scroll.x,
|
||||
screen_pos.y - origin.y - config_.draw_offset.y - scroll.y);
|
||||
|
||||
if (local.x < 0.0f || local.y < 0.0f) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const int column = static_cast<int>(local.x / tile_display_size);
|
||||
const int row = static_cast<int>(local.y / tile_display_size);
|
||||
|
||||
return row * config_.tiles_per_row + column;
|
||||
}
|
||||
|
||||
void TileSelectorWidget::DrawHighlight(int tile_display_size) const {
|
||||
if (!canvas_ || !IsValidTileId(selected_tile_id_)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int column = selected_tile_id_ % config_.tiles_per_row;
|
||||
const int row = selected_tile_id_ / config_.tiles_per_row;
|
||||
|
||||
const float x = config_.draw_offset.x + column * tile_display_size;
|
||||
const float y = config_.draw_offset.y + row * tile_display_size;
|
||||
|
||||
canvas_->DrawOutlineWithColor(static_cast<int>(x), static_cast<int>(y),
|
||||
tile_display_size, tile_display_size,
|
||||
config_.highlight_color);
|
||||
}
|
||||
|
||||
void TileSelectorWidget::DrawTileIdLabels(int) const {
|
||||
// Future enhancement: draw ImGui text overlay with tile indices.
|
||||
}
|
||||
|
||||
void TileSelectorWidget::ScrollToTile(int tile_id, bool use_imgui_scroll) {
|
||||
if (!canvas_ || !IsValidTileId(tile_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer scroll until next render (when we're in the correct ImGui window context)
|
||||
pending_scroll_tile_id_ = tile_id;
|
||||
pending_scroll_use_imgui_ = use_imgui_scroll;
|
||||
}
|
||||
|
||||
ImVec2 TileSelectorWidget::TileOrigin(int tile_id) const {
|
||||
if (!IsValidTileId(tile_id)) {
|
||||
return ImVec2(-1, -1);
|
||||
}
|
||||
const int tile_display_size =
|
||||
static_cast<int>(config_.tile_size * config_.display_scale);
|
||||
const int column = tile_id % config_.tiles_per_row;
|
||||
const int row = tile_id / config_.tiles_per_row;
|
||||
return ImVec2(config_.draw_offset.x + column * tile_display_size,
|
||||
config_.draw_offset.y + row * tile_display_size);
|
||||
}
|
||||
|
||||
bool TileSelectorWidget::IsValidTileId(int tile_id) const {
|
||||
return tile_id >= 0 && tile_id < total_tiles_;
|
||||
}
|
||||
|
||||
} // namespace yaze::gui
|
||||
|
||||
|
||||
71
src/app/gui/widgets/tile_selector_widget.h
Normal file
71
src/app/gui/widgets/tile_selector_widget.h
Normal file
@@ -0,0 +1,71 @@
|
||||
#ifndef YAZE_APP_GUI_WIDGETS_TILE_SELECTOR_WIDGET_H
|
||||
#define YAZE_APP_GUI_WIDGETS_TILE_SELECTOR_WIDGET_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "app/gfx/bitmap.h"
|
||||
#include "app/gui/canvas.h"
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
namespace yaze::gui {
|
||||
|
||||
/**
|
||||
* @brief Reusable tile selector built on top of Canvas.
|
||||
*
|
||||
* Minimal mutable state, designed for reuse across editors and automation.
|
||||
*/
|
||||
class TileSelectorWidget {
|
||||
public:
|
||||
struct Config {
|
||||
int tile_size = 16;
|
||||
float display_scale = 2.0f;
|
||||
int tiles_per_row = 8;
|
||||
int total_tiles = 512;
|
||||
ImVec2 draw_offset = {2.0f, 0.0f};
|
||||
bool show_tile_ids = false;
|
||||
ImVec4 highlight_color = {1.0f, 0.85f, 0.35f, 1.0f};
|
||||
};
|
||||
|
||||
struct RenderResult {
|
||||
bool tile_clicked = false;
|
||||
bool tile_double_clicked = false;
|
||||
bool selection_changed = false;
|
||||
int selected_tile = -1;
|
||||
};
|
||||
|
||||
explicit TileSelectorWidget(std::string widget_id);
|
||||
TileSelectorWidget(std::string widget_id, Config config);
|
||||
|
||||
void AttachCanvas(Canvas* canvas);
|
||||
void SetTileCount(int total_tiles);
|
||||
void SetSelectedTile(int tile_id);
|
||||
int GetSelectedTileID() const { return selected_tile_id_; }
|
||||
|
||||
RenderResult Render(gfx::Bitmap& atlas, bool atlas_ready);
|
||||
|
||||
void ScrollToTile(int tile_id, bool use_imgui_scroll = true);
|
||||
ImVec2 TileOrigin(int tile_id) const;
|
||||
|
||||
private:
|
||||
RenderResult HandleInteraction(int tile_display_size);
|
||||
int ResolveTileAtCursor(int tile_display_size) const;
|
||||
void DrawHighlight(int tile_display_size) const;
|
||||
void DrawTileIdLabels(int tile_display_size) const;
|
||||
bool IsValidTileId(int tile_id) const;
|
||||
|
||||
Canvas* canvas_ = nullptr;
|
||||
Config config_{};
|
||||
int selected_tile_id_ = 0;
|
||||
int total_tiles_ = 0;
|
||||
std::string widget_id_;
|
||||
|
||||
// Deferred scroll state (for when ScrollToTile is called outside render context)
|
||||
mutable int pending_scroll_tile_id_ = -1;
|
||||
mutable bool pending_scroll_use_imgui_ = true;
|
||||
};
|
||||
|
||||
} // namespace yaze::gui
|
||||
|
||||
#endif // YAZE_APP_GUI_WIDGETS_TILE_SELECTOR_WIDGET_H
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ if(YAZE_BUILD_TESTS AND NOT YAZE_BUILD_TESTS STREQUAL "OFF")
|
||||
unit/gfx/snes_tile_test.cc
|
||||
unit/gfx/compression_test.cc
|
||||
unit/gfx/snes_palette_test.cc
|
||||
unit/gui/tile_selector_widget_test.cc
|
||||
unit/zelda3/message_test.cc
|
||||
unit/zelda3/overworld_test.cc
|
||||
unit/zelda3/object_parser_test.cc
|
||||
@@ -97,6 +98,7 @@ if(YAZE_BUILD_TESTS AND NOT YAZE_BUILD_TESTS STREQUAL "OFF")
|
||||
unit/gfx/snes_tile_test.cc
|
||||
unit/gfx/compression_test.cc
|
||||
unit/gfx/snes_palette_test.cc
|
||||
unit/gui/tile_selector_widget_test.cc
|
||||
unit/zelda3/message_test.cc
|
||||
unit/zelda3/overworld_test.cc
|
||||
unit/zelda3/object_parser_test.cc
|
||||
|
||||
@@ -79,6 +79,7 @@ source_group("Tests\\Unit" FILES
|
||||
unit/gfx/snes_tile_test.cc
|
||||
unit/gfx/compression_test.cc
|
||||
unit/gfx/snes_palette_test.cc
|
||||
unit/gui/tile_selector_widget_test.cc
|
||||
unit/zelda3/message_test.cc
|
||||
unit/zelda3/overworld_test.cc
|
||||
unit/zelda3/object_parser_test.cc
|
||||
|
||||
194
test/unit/gui/tile_selector_widget_test.cc
Normal file
194
test/unit/gui/tile_selector_widget_test.cc
Normal file
@@ -0,0 +1,194 @@
|
||||
#include "app/gui/widgets/tile_selector_widget.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/gfx/bitmap.h"
|
||||
#include "app/gui/canvas.h"
|
||||
#include "testing.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
using ::testing::Eq;
|
||||
using ::testing::NotNull;
|
||||
|
||||
class TileSelectorWidgetTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Create a test canvas
|
||||
canvas_ = std::make_unique<gui::Canvas>("TestCanvas", ImVec2(512, 512),
|
||||
gui::CanvasGridSize::k16x16);
|
||||
|
||||
// Create a test config
|
||||
config_.tile_size = 16;
|
||||
config_.display_scale = 2.0f;
|
||||
config_.tiles_per_row = 8;
|
||||
config_.total_tiles = 64; // 8x8 grid
|
||||
config_.draw_offset = {2.0f, 0.0f};
|
||||
config_.show_tile_ids = false;
|
||||
config_.highlight_color = {1.0f, 0.85f, 0.35f, 1.0f};
|
||||
}
|
||||
|
||||
std::unique_ptr<gui::Canvas> canvas_;
|
||||
gui::TileSelectorWidget::Config config_;
|
||||
};
|
||||
|
||||
// Test basic construction
|
||||
TEST_F(TileSelectorWidgetTest, Construction) {
|
||||
gui::TileSelectorWidget widget("test_widget");
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 0);
|
||||
}
|
||||
|
||||
// Test construction with config
|
||||
TEST_F(TileSelectorWidgetTest, ConstructionWithConfig) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 0);
|
||||
}
|
||||
|
||||
// Test canvas attachment
|
||||
TEST_F(TileSelectorWidgetTest, AttachCanvas) {
|
||||
gui::TileSelectorWidget widget("test_widget");
|
||||
widget.AttachCanvas(canvas_.get());
|
||||
// No crash means success
|
||||
}
|
||||
|
||||
// Test tile count setting
|
||||
TEST_F(TileSelectorWidgetTest, SetTileCount) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.SetTileCount(128);
|
||||
// Verify selection is clamped when tile count changes
|
||||
widget.SetSelectedTile(100);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 100);
|
||||
|
||||
// Setting tile count lower should clamp selection
|
||||
widget.SetTileCount(50);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 0); // Should reset to 0
|
||||
}
|
||||
|
||||
// Test selected tile setting
|
||||
TEST_F(TileSelectorWidgetTest, SetSelectedTile) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.SetTileCount(64);
|
||||
|
||||
widget.SetSelectedTile(10);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 10);
|
||||
|
||||
widget.SetSelectedTile(63);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 63);
|
||||
|
||||
// Out of bounds should be ignored
|
||||
widget.SetSelectedTile(64);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 63); // Should remain unchanged
|
||||
|
||||
widget.SetSelectedTile(-1);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 63); // Should remain unchanged
|
||||
}
|
||||
|
||||
// Test tile origin calculation
|
||||
TEST_F(TileSelectorWidgetTest, TileOrigin) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.SetTileCount(64);
|
||||
|
||||
// Test first tile (0,0)
|
||||
auto origin = widget.TileOrigin(0);
|
||||
EXPECT_FLOAT_EQ(origin.x, config_.draw_offset.x);
|
||||
EXPECT_FLOAT_EQ(origin.y, config_.draw_offset.y);
|
||||
|
||||
// Test tile at (1,0)
|
||||
origin = widget.TileOrigin(1);
|
||||
float expected_x = config_.draw_offset.x +
|
||||
(config_.tile_size * config_.display_scale);
|
||||
EXPECT_FLOAT_EQ(origin.x, expected_x);
|
||||
EXPECT_FLOAT_EQ(origin.y, config_.draw_offset.y);
|
||||
|
||||
// Test tile at (0,1) - first tile of second row
|
||||
origin = widget.TileOrigin(8);
|
||||
expected_x = config_.draw_offset.x;
|
||||
float expected_y = config_.draw_offset.y +
|
||||
(config_.tile_size * config_.display_scale);
|
||||
EXPECT_FLOAT_EQ(origin.x, expected_x);
|
||||
EXPECT_FLOAT_EQ(origin.y, expected_y);
|
||||
|
||||
// Test invalid tile ID
|
||||
origin = widget.TileOrigin(64);
|
||||
EXPECT_FLOAT_EQ(origin.x, -1.0f);
|
||||
EXPECT_FLOAT_EQ(origin.y, -1.0f);
|
||||
}
|
||||
|
||||
// Test render without atlas (should not crash)
|
||||
TEST_F(TileSelectorWidgetTest, RenderWithoutAtlas) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.AttachCanvas(canvas_.get());
|
||||
|
||||
gfx::Bitmap atlas;
|
||||
auto result = widget.Render(atlas, false);
|
||||
|
||||
EXPECT_FALSE(result.tile_clicked);
|
||||
EXPECT_FALSE(result.tile_double_clicked);
|
||||
EXPECT_FALSE(result.selection_changed);
|
||||
EXPECT_EQ(result.selected_tile, -1);
|
||||
}
|
||||
|
||||
// Test programmatic selection for AI/automation
|
||||
TEST_F(TileSelectorWidgetTest, ProgrammaticSelection) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.AttachCanvas(canvas_.get());
|
||||
widget.SetTileCount(64);
|
||||
|
||||
// Simulate AI/automation selecting tiles programmatically
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
widget.SetSelectedTile(i);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), i);
|
||||
|
||||
auto origin = widget.TileOrigin(i);
|
||||
int expected_col = i % config_.tiles_per_row;
|
||||
int expected_row = i / config_.tiles_per_row;
|
||||
float expected_x = config_.draw_offset.x +
|
||||
expected_col * config_.tile_size * config_.display_scale;
|
||||
float expected_y = config_.draw_offset.y +
|
||||
expected_row * config_.tile_size * config_.display_scale;
|
||||
|
||||
EXPECT_FLOAT_EQ(origin.x, expected_x);
|
||||
EXPECT_FLOAT_EQ(origin.y, expected_y);
|
||||
}
|
||||
}
|
||||
|
||||
// Test scroll to tile
|
||||
TEST_F(TileSelectorWidgetTest, ScrollToTile) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.AttachCanvas(canvas_.get());
|
||||
widget.SetTileCount(64);
|
||||
|
||||
// Scroll to various tiles (should not crash)
|
||||
widget.ScrollToTile(0);
|
||||
widget.ScrollToTile(10);
|
||||
widget.ScrollToTile(63);
|
||||
|
||||
// Invalid tile should not crash
|
||||
widget.ScrollToTile(-1);
|
||||
widget.ScrollToTile(64);
|
||||
}
|
||||
|
||||
// Test different configs
|
||||
TEST_F(TileSelectorWidgetTest, DifferentConfigs) {
|
||||
// Test with 16x16 grid
|
||||
gui::TileSelectorWidget::Config large_config;
|
||||
large_config.tile_size = 8;
|
||||
large_config.display_scale = 1.0f;
|
||||
large_config.tiles_per_row = 16;
|
||||
large_config.total_tiles = 256;
|
||||
large_config.draw_offset = {0.0f, 0.0f};
|
||||
|
||||
gui::TileSelectorWidget large_widget("large_widget", large_config);
|
||||
large_widget.SetTileCount(256);
|
||||
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
large_widget.SetSelectedTile(i);
|
||||
EXPECT_EQ(large_widget.GetSelectedTileID(), i);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
|
||||
Reference in New Issue
Block a user