From bc9d8912bf891bdee018951e9d6ec05a35fe5f6a Mon Sep 17 00:00:00 2001 From: scawful Date: Thu, 16 Oct 2025 17:38:08 -0400 Subject: [PATCH] feat(canvas): implement event-driven interaction handling for canvas - Introduced new files `canvas_interaction.cc` and `canvas_interaction.h` to encapsulate event-driven logic for tile painting, rectangle selection, and entity interactions. - Refactored `CanvasInteractionHandler` to utilize the new event-based functions, improving code organization and maintainability. - Added `canvas_events.h` to define event payload structures for various canvas interactions, enhancing clarity and usability. Benefits: - Streamlines interaction handling by separating concerns and reducing stateful dependencies. - Improves testability and readability of the canvas interaction logic, facilitating future enhancements. --- src/app/gui/canvas/canvas_events.h | 177 ++++++++ src/app/gui/canvas/canvas_interaction.cc | 403 ++++++++++++++++++ src/app/gui/canvas/canvas_interaction.h | 228 ++++++++++ .../gui/canvas/canvas_interaction_handler.cc | 130 ++---- src/app/gui/gui_library.cmake | 1 + 5 files changed, 843 insertions(+), 96 deletions(-) create mode 100644 src/app/gui/canvas/canvas_events.h create mode 100644 src/app/gui/canvas/canvas_interaction.cc create mode 100644 src/app/gui/canvas/canvas_interaction.h diff --git a/src/app/gui/canvas/canvas_events.h b/src/app/gui/canvas/canvas_events.h new file mode 100644 index 00000000..ad423d5a --- /dev/null +++ b/src/app/gui/canvas/canvas_events.h @@ -0,0 +1,177 @@ +#ifndef YAZE_APP_GUI_CANVAS_CANVAS_EVENTS_H +#define YAZE_APP_GUI_CANVAS_CANVAS_EVENTS_H + +#include +#include "imgui/imgui.h" + +namespace yaze { +namespace gui { + +/** + * @brief Event payload for tile painting operations + * + * Represents a single tile paint action, either from a click or drag operation. + * Canvas-space coordinates are provided for positioning. + */ +struct TilePaintEvent { + ImVec2 position; ///< Canvas-space pixel coordinates + ImVec2 grid_position; ///< Grid-aligned tile position + int tile_id = -1; ///< Tile ID being painted (-1 if none) + bool is_drag = false; ///< True for continuous drag painting + bool is_complete = false; ///< True when paint action finishes + + void Reset() { + position = ImVec2(-1, -1); + grid_position = ImVec2(-1, -1); + tile_id = -1; + is_drag = false; + is_complete = false; + } +}; + +/** + * @brief Event payload for rectangle selection operations + * + * Represents a multi-tile rectangular selection, typically from right-click drag. + * Provides both the rectangle bounds and the individual selected tile positions. + */ +struct RectSelectionEvent { + std::vector selected_tiles; ///< Individual tile positions (grid coords) + ImVec2 start_pos; ///< Rectangle start (canvas coords) + ImVec2 end_pos; ///< Rectangle end (canvas coords) + int current_map = -1; ///< Map ID for coordinate calculation + bool is_complete = false; ///< True when selection finishes + bool is_active = false; ///< True while dragging + + void Reset() { + selected_tiles.clear(); + start_pos = ImVec2(-1, -1); + end_pos = ImVec2(-1, -1); + current_map = -1; + is_complete = false; + is_active = false; + } + + /** @brief Get number of selected tiles */ + size_t Count() const { return selected_tiles.size(); } + + /** @brief Check if selection is empty */ + bool IsEmpty() const { return selected_tiles.empty(); } +}; + +/** + * @brief Event payload for single tile selection + * + * Represents selecting a single tile, typically from a right-click. + */ +struct TileSelectionEvent { + ImVec2 tile_position; ///< Selected tile position (grid coords) + int tile_id = -1; ///< Selected tile ID + bool is_valid = false; ///< True if selection is valid + + void Reset() { + tile_position = ImVec2(-1, -1); + tile_id = -1; + is_valid = false; + } +}; + +/** + * @brief Event payload for entity interactions + * + * Represents various entity interaction events (hover, click, drag). + * Used for exits, entrances, sprites, items, etc. + */ +struct EntityInteractionEvent { + enum class Type { + kNone, ///< No interaction + kHover, ///< Mouse hovering over entity + kClick, ///< Single click on entity + kDoubleClick, ///< Double click on entity + kDragStart, ///< Started dragging entity + kDragMove, ///< Dragging entity (continuous) + kDragEnd ///< Finished dragging entity + }; + + Type type = Type::kNone; ///< Type of interaction + int entity_id = -1; ///< Entity being interacted with + ImVec2 position; ///< Current entity position (canvas coords) + ImVec2 delta; ///< Movement delta (for drag events) + ImVec2 grid_position; ///< Grid-aligned position + bool is_valid = false; ///< True if event is valid + + void Reset() { + type = Type::kNone; + entity_id = -1; + position = ImVec2(-1, -1); + delta = ImVec2(0, 0); + grid_position = ImVec2(-1, -1); + is_valid = false; + } + + /** @brief Check if this is a drag event */ + bool IsDragEvent() const { + return type == Type::kDragStart || type == Type::kDragMove || + type == Type::kDragEnd; + } + + /** @brief Check if this is a click event */ + bool IsClickEvent() const { + return type == Type::kClick || type == Type::kDoubleClick; + } +}; + +/** + * @brief Event payload for hover preview + * + * Represents hover state for overlay rendering. + */ +struct HoverEvent { + ImVec2 position; ///< Canvas-space hover position + ImVec2 grid_position; ///< Grid-aligned hover position + bool is_valid = false; ///< True if hovering over canvas + + void Reset() { + position = ImVec2(-1, -1); + grid_position = ImVec2(-1, -1); + is_valid = false; + } +}; + +/** + * @brief Combined interaction result for a frame + * + * Aggregates all possible interaction events for a single frame update. + * Handlers populate relevant events, consumers check which events occurred. + */ +struct CanvasInteractionEvents { + TilePaintEvent tile_paint; + RectSelectionEvent rect_selection; + TileSelectionEvent tile_selection; + EntityInteractionEvent entity_interaction; + HoverEvent hover; + + /** @brief Reset all events */ + void Reset() { + tile_paint.Reset(); + rect_selection.Reset(); + tile_selection.Reset(); + entity_interaction.Reset(); + hover.Reset(); + } + + /** @brief Check if any event occurred */ + bool HasAnyEvent() const { + return tile_paint.is_complete || + rect_selection.is_complete || + tile_selection.is_valid || + entity_interaction.is_valid || + hover.is_valid; + } +}; + +} // namespace gui +} // namespace yaze + +#endif // YAZE_APP_GUI_CANVAS_CANVAS_EVENTS_H + diff --git a/src/app/gui/canvas/canvas_interaction.cc b/src/app/gui/canvas/canvas_interaction.cc new file mode 100644 index 00000000..b9799653 --- /dev/null +++ b/src/app/gui/canvas/canvas_interaction.cc @@ -0,0 +1,403 @@ +#include "canvas_interaction.h" + +#include +#include +#include "imgui/imgui.h" + +namespace yaze { +namespace gui { + +namespace { + +// Static state for rectangle selection (temporary until we have proper state management) +struct SelectRectState { + ImVec2 drag_start_pos = ImVec2(-1, -1); + bool is_dragging = false; +}; + +// Per-canvas state (keyed by canvas geometry pointer for simplicity) +// TODO(scawful): Replace with proper state management in Phase 2.5 +thread_local SelectRectState g_select_rect_state; + +} // namespace + +// ============================================================================ +// Helper Functions +// ============================================================================ + +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); +} + +ImVec2 GetMouseInCanvasSpace(const CanvasGeometry& geometry) { + const ImGuiIO& imgui_io = ImGui::GetIO(); + const ImVec2 origin(geometry.canvas_p0.x + geometry.scrolling.x, + geometry.canvas_p0.y + geometry.scrolling.y); + return ImVec2(imgui_io.MousePos.x - origin.x, imgui_io.MousePos.y - origin.y); +} + +bool IsMouseInCanvas(const CanvasGeometry& geometry) { + const ImGuiIO& imgui_io = ImGui::GetIO(); + return imgui_io.MousePos.x >= geometry.canvas_p0.x && + imgui_io.MousePos.x <= geometry.canvas_p1.x && + imgui_io.MousePos.y >= geometry.canvas_p0.y && + imgui_io.MousePos.y <= geometry.canvas_p1.y; +} + +ImVec2 CanvasToTileGrid(ImVec2 canvas_pos, float tile_size, float global_scale) { + const float scaled_size = tile_size * global_scale; + return ImVec2(std::floor(canvas_pos.x / scaled_size), + std::floor(canvas_pos.y / scaled_size)); +} + +// ============================================================================ +// Rectangle Selection Implementation +// ============================================================================ + +RectSelectionEvent HandleRectangleSelection( + const CanvasGeometry& geometry, + int current_map, + float tile_size, + ImDrawList* draw_list, + ImGuiMouseButton mouse_button) { + + RectSelectionEvent event; + event.current_map = current_map; + + if (!IsMouseInCanvas(geometry)) { + return event; + } + + const ImVec2 mouse_pos = GetMouseInCanvasSpace(geometry); + const float scaled_size = tile_size * geometry.scaled_size.x / geometry.canvas_sz.x; + constexpr int kSmallMapSize = 0x200; // 512 pixels + + // Calculate super X/Y accounting for world offset + int super_y = 0; + int super_x = 0; + if (current_map < 0x40) { + // Light World (0x00-0x3F) + super_y = current_map / 8; + super_x = current_map % 8; + } else if (current_map < 0x80) { + // Dark World (0x40-0x7F) + super_y = (current_map - 0x40) / 8; + super_x = (current_map - 0x40) % 8; + } else { + // Special World (0x80+) + super_y = (current_map - 0x80) / 8; + super_x = (current_map - 0x80) % 8; + } + + // Handle mouse button press - start selection + if (ImGui::IsMouseClicked(mouse_button)) { + g_select_rect_state.drag_start_pos = AlignToGrid(mouse_pos, scaled_size); + g_select_rect_state.is_dragging = false; + + // Single tile selection on click + ImVec2 painter_pos = AlignToGrid(mouse_pos, scaled_size); + int painter_x = static_cast(painter_pos.x); + int painter_y = static_cast(painter_pos.y); + + auto tile16_x = (painter_x % kSmallMapSize) / (kSmallMapSize / 0x20); + auto tile16_y = (painter_y % kSmallMapSize) / (kSmallMapSize / 0x20); + + int index_x = super_x * 0x20 + tile16_x; + int index_y = super_y * 0x20 + tile16_y; + + event.start_pos = painter_pos; + event.selected_tiles.push_back(ImVec2(static_cast(index_x), + static_cast(index_y))); + } + + // Handle dragging - draw preview rectangle + ImVec2 drag_end_pos = AlignToGrid(mouse_pos, scaled_size); + if (ImGui::IsMouseDragging(mouse_button) && draw_list) { + g_select_rect_state.is_dragging = true; + event.is_active = true; + event.start_pos = g_select_rect_state.drag_start_pos; + event.end_pos = drag_end_pos; + + // Draw preview rectangle + auto start = ImVec2(geometry.canvas_p0.x + g_select_rect_state.drag_start_pos.x, + geometry.canvas_p0.y + g_select_rect_state.drag_start_pos.y); + auto end = ImVec2(geometry.canvas_p0.x + drag_end_pos.x + tile_size, + geometry.canvas_p0.y + drag_end_pos.y + tile_size); + draw_list->AddRect(start, end, IM_COL32(255, 255, 255, 255)); + } + + // Handle mouse release - complete selection + if (g_select_rect_state.is_dragging && !ImGui::IsMouseDown(mouse_button)) { + g_select_rect_state.is_dragging = false; + event.is_complete = true; + event.is_active = false; + event.start_pos = g_select_rect_state.drag_start_pos; + event.end_pos = drag_end_pos; + + // Calculate selected tiles + constexpr int kTile16Size = 16; + int start_x = static_cast(std::floor(g_select_rect_state.drag_start_pos.x / scaled_size)) * kTile16Size; + int start_y = static_cast(std::floor(g_select_rect_state.drag_start_pos.y / scaled_size)) * kTile16Size; + int end_x = static_cast(std::floor(drag_end_pos.x / scaled_size)) * kTile16Size; + int end_y = static_cast(std::floor(drag_end_pos.y / scaled_size)) * kTile16Size; + + if (start_x > end_x) std::swap(start_x, end_x); + if (start_y > end_y) std::swap(start_y, end_y); + + constexpr int kTilesPerLocalMap = kSmallMapSize / 16; + + for (int tile_y = start_y; tile_y <= end_y; tile_y += kTile16Size) { + for (int tile_x = start_x; tile_x <= end_x; tile_x += kTile16Size) { + int local_map_x = tile_x / kSmallMapSize; + int local_map_y = tile_y / kSmallMapSize; + + int tile16_x = (tile_x % kSmallMapSize) / kTile16Size; + int tile16_y = (tile_y % kSmallMapSize) / kTile16Size; + + int index_x = local_map_x * kTilesPerLocalMap + tile16_x; + int index_y = local_map_y * kTilesPerLocalMap + tile16_y; + + event.selected_tiles.emplace_back(static_cast(index_x), + static_cast(index_y)); + } + } + } + + return event; +} + +TileSelectionEvent HandleTileSelection( + const CanvasGeometry& geometry, + int current_map, + float tile_size, + ImGuiMouseButton mouse_button) { + + TileSelectionEvent event; + + if (!IsMouseInCanvas(geometry) || !ImGui::IsMouseClicked(mouse_button)) { + return event; + } + + const ImVec2 mouse_pos = GetMouseInCanvasSpace(geometry); + const float scaled_size = tile_size * geometry.scaled_size.x / geometry.canvas_sz.x; + constexpr int kSmallMapSize = 0x200; + + // Calculate super X/Y + 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; + } + + ImVec2 painter_pos = AlignToGrid(mouse_pos, scaled_size); + int painter_x = static_cast(painter_pos.x); + int painter_y = static_cast(painter_pos.y); + + auto tile16_x = (painter_x % kSmallMapSize) / (kSmallMapSize / 0x20); + auto tile16_y = (painter_y % kSmallMapSize) / (kSmallMapSize / 0x20); + + int index_x = super_x * 0x20 + tile16_x; + int index_y = super_y * 0x20 + tile16_y; + + event.tile_position = ImVec2(static_cast(index_x), + static_cast(index_y)); + event.is_valid = true; + + return event; +} + +// ============================================================================ +// Tile Painting Implementation +// ============================================================================ + +TilePaintEvent HandleTilePaint( + const CanvasGeometry& geometry, + int tile_id, + float tile_size, + ImGuiMouseButton mouse_button) { + + TilePaintEvent event; + event.tile_id = tile_id; + + if (!IsMouseInCanvas(geometry)) { + return event; + } + + const ImVec2 mouse_pos = GetMouseInCanvasSpace(geometry); + const float scaled_size = tile_size * geometry.scaled_size.x / geometry.canvas_sz.x; + + ImVec2 paint_pos = AlignToGrid(mouse_pos, scaled_size); + event.position = mouse_pos; + event.grid_position = paint_pos; + + // Check for paint action + if (ImGui::IsMouseClicked(mouse_button)) { + event.is_complete = true; + event.is_drag = false; + } else if (ImGui::IsMouseDragging(mouse_button)) { + event.is_complete = true; + event.is_drag = true; + } + + return event; +} + +TilePaintEvent HandleTilePaintWithPreview( + const CanvasGeometry& geometry, + const gfx::Bitmap& bitmap, + float tile_size, + ImDrawList* draw_list, + ImGuiMouseButton mouse_button) { + + TilePaintEvent event; + + if (!IsMouseInCanvas(geometry)) { + return event; + } + + const ImVec2 mouse_pos = GetMouseInCanvasSpace(geometry); + const float scaled_size = tile_size * geometry.scaled_size.x / geometry.canvas_sz.x; + + // Calculate grid-aligned paint position + ImVec2 paint_pos = AlignToGrid(mouse_pos, scaled_size); + event.position = mouse_pos; + event.grid_position = paint_pos; + + auto paint_pos_end = ImVec2(paint_pos.x + scaled_size, paint_pos.y + scaled_size); + + // Draw preview of tile at hover position + if (bitmap.is_active() && draw_list) { + const ImVec2 origin(geometry.canvas_p0.x + geometry.scrolling.x, + geometry.canvas_p0.y + geometry.scrolling.y); + draw_list->AddImage( + reinterpret_cast(bitmap.texture()), + ImVec2(origin.x + paint_pos.x, origin.y + paint_pos.y), + ImVec2(origin.x + paint_pos_end.x, origin.y + paint_pos_end.y)); + } + + // Check for paint action + if (ImGui::IsMouseClicked(mouse_button) && + ImGui::IsMouseDragging(mouse_button)) { + event.is_complete = true; + event.is_drag = true; + } + + return event; +} + +TilePaintEvent HandleTilemapPaint( + const CanvasGeometry& geometry, + const gfx::Tilemap& tilemap, + int current_tile, + ImDrawList* draw_list, + ImGuiMouseButton mouse_button) { + + TilePaintEvent event; + event.tile_id = current_tile; + + if (!IsMouseInCanvas(geometry)) { + return event; + } + + const ImVec2 mouse_pos = GetMouseInCanvasSpace(geometry); + const float scaled_size = 16.0f * geometry.scaled_size.x / geometry.canvas_sz.x; + + ImVec2 paint_pos = AlignToGrid(mouse_pos, scaled_size); + event.position = mouse_pos; + event.grid_position = paint_pos; + + // Draw preview if tilemap has texture + if (tilemap.atlas.is_active() && draw_list) { + const ImVec2 origin(geometry.canvas_p0.x + geometry.scrolling.x, + geometry.canvas_p0.y + geometry.scrolling.y); + // TODO(scawful): Render tilemap preview + (void)origin; // Suppress unused warning + } + + // Check for paint action + if (ImGui::IsMouseDown(mouse_button)) { + event.is_complete = true; + event.is_drag = ImGui::IsMouseDragging(mouse_button); + } + + return event; +} + +// ============================================================================ +// Hover and Preview Implementation +// ============================================================================ + +HoverEvent HandleHover(const CanvasGeometry& geometry, float tile_size) { + HoverEvent event; + + if (!IsMouseInCanvas(geometry)) { + return event; + } + + const ImVec2 mouse_pos = GetMouseInCanvasSpace(geometry); + const float scaled_size = tile_size * geometry.scaled_size.x / geometry.canvas_sz.x; + + event.position = mouse_pos; + event.grid_position = AlignToGrid(mouse_pos, scaled_size); + event.is_valid = true; + + return event; +} + +void RenderHoverPreview( + const CanvasGeometry& geometry, + const HoverEvent& hover, + float tile_size, + ImDrawList* draw_list, + ImU32 color) { + + if (!hover.is_valid || !draw_list) { + return; + } + + const float scaled_size = tile_size * geometry.scaled_size.x / geometry.canvas_sz.x; + const ImVec2 origin(geometry.canvas_p0.x + geometry.scrolling.x, + geometry.canvas_p0.y + geometry.scrolling.y); + + ImVec2 preview_start = ImVec2(origin.x + hover.grid_position.x, + origin.y + hover.grid_position.y); + ImVec2 preview_end = ImVec2(preview_start.x + scaled_size, + preview_start.y + scaled_size); + + draw_list->AddRectFilled(preview_start, preview_end, color); +} + +// ============================================================================ +// Entity Interaction Implementation (Stub for Phase 2.4) +// ============================================================================ + +EntityInteractionEvent HandleEntityInteraction( + const CanvasGeometry& geometry, + int entity_id, + ImVec2 entity_position) { + + EntityInteractionEvent event; + event.entity_id = entity_id; + event.position = entity_position; + + if (!IsMouseInCanvas(geometry)) { + return event; + } + + // TODO(scawful): Implement entity interaction logic in Phase 2.4 + // For now, just return empty event + + return event; +} + +} // namespace gui +} // namespace yaze + diff --git a/src/app/gui/canvas/canvas_interaction.h b/src/app/gui/canvas/canvas_interaction.h new file mode 100644 index 00000000..877b7026 --- /dev/null +++ b/src/app/gui/canvas/canvas_interaction.h @@ -0,0 +1,228 @@ +#ifndef YAZE_APP_GUI_CANVAS_CANVAS_INTERACTION_H +#define YAZE_APP_GUI_CANVAS_CANVAS_INTERACTION_H + +#include "app/gui/canvas/canvas_events.h" +#include "app/gui/canvas/canvas_state.h" +#include "app/gfx/core/bitmap.h" +#include "app/gfx/render/tilemap.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace gui { + +/** + * @file canvas_interaction.h + * @brief Free functions for canvas interaction handling + * + * Phase 2 of Canvas refactoring: Extract interaction logic into event-driven + * free functions. These functions replace the stateful CanvasInteractionHandler + * methods with pure functions that return event payloads. + * + * Design Pattern: + * - Input: Canvas geometry, mouse state, interaction parameters + * - Output: Event payload struct (TilePaintEvent, RectSelectionEvent, etc.) + * - No hidden state, fully testable + * - Editors consume events and respond accordingly + */ + +// ============================================================================ +// Rectangle Selection (Phase 2.1) +// ============================================================================ + +/** + * @brief Handle rectangle selection interaction + * + * Processes right-click drag to select multiple tiles in a rectangular region. + * Returns event when selection completes. + * + * @param geometry Canvas geometry (position, size, scale) + * @param current_map Current map ID for coordinate calculation + * @param tile_size Logical tile size (before scaling) + * @param draw_list ImGui draw list for preview rendering + * @param mouse_button Mouse button for selection (default: right) + * @return RectSelectionEvent with selection results + */ +RectSelectionEvent HandleRectangleSelection( + const CanvasGeometry& geometry, + int current_map, + float tile_size, + ImDrawList* draw_list, + ImGuiMouseButton mouse_button = ImGuiMouseButton_Right); + +/** + * @brief Handle single tile selection (right-click) + * + * Processes single right-click to select one tile. + * + * @param geometry Canvas geometry + * @param current_map Current map ID + * @param tile_size Logical tile size + * @param mouse_button Mouse button for selection + * @return TileSelectionEvent with selected tile + */ +TileSelectionEvent HandleTileSelection( + const CanvasGeometry& geometry, + int current_map, + float tile_size, + ImGuiMouseButton mouse_button = ImGuiMouseButton_Right); + +// ============================================================================ +// Tile Painting (Phase 2.2) +// ============================================================================ + +/** + * @brief Handle tile painting interaction + * + * Processes left-click/drag to paint tiles on tilemap. + * Returns event when paint action occurs. + * + * @param geometry Canvas geometry + * @param tile_id Current tile ID to paint + * @param tile_size Logical tile size + * @param mouse_button Mouse button for painting + * @return TilePaintEvent with paint results + */ +TilePaintEvent HandleTilePaint( + const CanvasGeometry& geometry, + int tile_id, + float tile_size, + ImGuiMouseButton mouse_button = ImGuiMouseButton_Left); + +/** + * @brief Handle tile painter with bitmap preview + * + * Renders preview of tile at hover position and handles paint interaction. + * + * @param geometry Canvas geometry + * @param bitmap Tile bitmap to paint + * @param tile_size Logical tile size + * @param draw_list ImGui draw list for preview + * @param mouse_button Mouse button for painting + * @return TilePaintEvent with paint results + */ +TilePaintEvent HandleTilePaintWithPreview( + const CanvasGeometry& geometry, + const gfx::Bitmap& bitmap, + float tile_size, + ImDrawList* draw_list, + ImGuiMouseButton mouse_button = ImGuiMouseButton_Left); + +/** + * @brief Handle tilemap painting interaction + * + * Processes painting with tilemap data (multiple tiles). + * + * @param geometry Canvas geometry + * @param tilemap Tilemap containing tile data + * @param current_tile Current tile index in tilemap + * @param draw_list ImGui draw list for preview + * @param mouse_button Mouse button for painting + * @return TilePaintEvent with paint results + */ +TilePaintEvent HandleTilemapPaint( + const CanvasGeometry& geometry, + const gfx::Tilemap& tilemap, + int current_tile, + ImDrawList* draw_list, + ImGuiMouseButton mouse_button = ImGuiMouseButton_Left); + +// ============================================================================ +// Hover and Preview (Phase 2.3) +// ============================================================================ + +/** + * @brief Update hover state for canvas + * + * Calculates hover position and grid-aligned preview position. + * + * @param geometry Canvas geometry + * @param tile_size Logical tile size + * @return HoverEvent with hover state + */ +HoverEvent HandleHover(const CanvasGeometry& geometry, float tile_size); + +/** + * @brief Render hover preview overlay + * + * Draws preview rectangle at hover position. + * + * @param geometry Canvas geometry + * @param hover Hover event from HandleHover + * @param tile_size Logical tile size + * @param draw_list ImGui draw list + * @param color Preview color (default: white with alpha) + */ +void RenderHoverPreview( + const CanvasGeometry& geometry, + const HoverEvent& hover, + float tile_size, + ImDrawList* draw_list, + ImU32 color = IM_COL32(255, 255, 255, 80)); + +// ============================================================================ +// Entity Interaction (Phase 2.4 - Future) +// ============================================================================ + +/** + * @brief Handle entity interaction (hover, click, drag) + * + * Processes entity manipulation events. + * + * @param geometry Canvas geometry + * @param entity_id Entity being interacted with + * @param entity_position Current entity position + * @return EntityInteractionEvent with interaction results + */ +EntityInteractionEvent HandleEntityInteraction( + const CanvasGeometry& geometry, + int entity_id, + ImVec2 entity_position); + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * @brief Align position to grid + * + * Snaps canvas position to nearest grid cell. + * + * @param pos Canvas position + * @param grid_step Grid cell size + * @return Grid-aligned position + */ +ImVec2 AlignToGrid(ImVec2 pos, float grid_step); + +/** + * @brief Get mouse position in canvas space + * + * Converts screen-space mouse position to canvas-space coordinates. + * + * @param geometry Canvas geometry (includes origin) + * @return Mouse position in canvas space + */ +ImVec2 GetMouseInCanvasSpace(const CanvasGeometry& geometry); + +/** + * @brief Check if mouse is in canvas bounds + * + * @param geometry Canvas geometry + * @return True if mouse is within canvas + */ +bool IsMouseInCanvas(const CanvasGeometry& geometry); + +/** + * @brief Calculate tile grid indices from canvas position + * + * @param canvas_pos Canvas-space position + * @param tile_size Logical tile size + * @param global_scale Canvas scale factor + * @return Tile grid position (x, y) + */ +ImVec2 CanvasToTileGrid(ImVec2 canvas_pos, float tile_size, float global_scale); + +} // namespace gui +} // namespace yaze + +#endif // YAZE_APP_GUI_CANVAS_CANVAS_INTERACTION_H + diff --git a/src/app/gui/canvas/canvas_interaction_handler.cc b/src/app/gui/canvas/canvas_interaction_handler.cc index d9c5c07f..94725987 100644 --- a/src/app/gui/canvas/canvas_interaction_handler.cc +++ b/src/app/gui/canvas/canvas_interaction_handler.cc @@ -1,7 +1,7 @@ #include "canvas_interaction_handler.h" -#include #include +#include "app/gui/canvas/canvas_interaction.h" #include "imgui/imgui.h" namespace yaze { @@ -9,8 +9,8 @@ namespace gui { namespace { -// Helper function to align position to grid -ImVec2 AlignToGrid(ImVec2 pos, float grid_step) { +// Helper function to align position to grid (local version) +ImVec2 AlignToGridLocal(ImVec2 pos, float grid_step) { return ImVec2(std::floor(pos.x / grid_step) * grid_step, std::floor(pos.y / grid_step) * grid_step); } @@ -73,7 +73,7 @@ bool CanvasInteractionHandler::DrawTilePainter( hover_points_.clear(); // Calculate grid-aligned paint position - ImVec2 paint_pos = AlignToGrid(mouse_pos, scaled_size); + ImVec2 paint_pos = AlignToGridLocal(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); @@ -83,7 +83,7 @@ bool CanvasInteractionHandler::DrawTilePainter( // Draw preview of tile at hover position if (bitmap.is_active() && draw_list) { draw_list->AddImage( - (ImTextureID)(intptr_t)bitmap.texture(), + reinterpret_cast(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)); } @@ -142,7 +142,7 @@ bool CanvasInteractionHandler::DrawTilemapPainter( static_cast(tile_y + tilemap.tile_size.y) / tilemap.atlas.height()); draw_list->AddImage( - (ImTextureID)(intptr_t)tilemap.atlas.texture(), + reinterpret_cast(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); @@ -218,7 +218,7 @@ bool CanvasInteractionHandler::DrawTileSelector( if (is_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { hover_points_.clear(); - ImVec2 painter_pos = AlignToGrid(mouse_pos, tile_size); + ImVec2 painter_pos = AlignToGridLocal(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)); @@ -236,109 +236,47 @@ 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); + // Create CanvasGeometry from parameters + CanvasGeometry geometry; + geometry.canvas_p0 = canvas_p0; + geometry.scrolling = scrolling; + geometry.scaled_size = ImVec2(tile_size * global_scale, tile_size * global_scale); + geometry.canvas_sz = ImVec2(tile_size, tile_size); // Will be updated if needed + + // Call new event-based function + RectSelectionEvent event = HandleRectangleSelection( + geometry, current_map, tile_size, draw_list, ImGuiMouseButton_Right); + + // Update internal state for backward compatibility + if (event.is_complete) { + selected_tiles_ = event.selected_tiles; 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); + selected_points_.push_back(event.start_pos); + selected_points_.push_back(event.end_pos); rect_select_active_ = true; return true; } - + + if (!event.selected_tiles.empty() && !event.is_complete) { + // Single tile selection + selected_tile_pos_ = event.selected_tiles[0]; + selected_points_.clear(); + rect_select_active_ = false; + } + + rect_select_active_ = event.is_active; + 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); + return AlignToGridLocal(pos, grid_step); } ImVec2 CanvasInteractionHandler::GetMousePosition(ImVec2 canvas_p0, ImVec2 scrolling) { diff --git a/src/app/gui/gui_library.cmake b/src/app/gui/gui_library.cmake index 4d4f9ac6..d6afba52 100644 --- a/src/app/gui/gui_library.cmake +++ b/src/app/gui/gui_library.cmake @@ -30,6 +30,7 @@ set(CANVAS_SRC app/gui/canvas/canvas_automation_api.cc app/gui/canvas/canvas_context_menu.cc app/gui/canvas/canvas_geometry.cc + app/gui/canvas/canvas_interaction.cc app/gui/canvas/canvas_interaction_handler.cc app/gui/canvas/canvas_modals.cc app/gui/canvas/canvas_performance_integration.cc