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:
scawful
2025-10-05 22:53:33 -04:00
parent 3200459c21
commit af2b698dbd
15 changed files with 1295 additions and 319 deletions

View File

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

View File

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

View 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

View 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

View File

@@ -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
)
# ==============================================================================

View File

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

View File

@@ -77,63 +77,8 @@ struct Table {
void AddTableColumn(Table &table, const std::string &label, GuiElement element);
void DrawTable(Table &params);
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 &params);
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

View 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

View 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