refactor(app): reorganize application structure and update includes

- Moved core components such as `Controller` and `Window` from `src/app/core/` to `src/app/` and `src/app/platform/`, respectively, to improve modularity and clarity.
- Updated include paths across the codebase to reflect the new locations of these components.
- Introduced a new foundational core library in `src/core/` for project management and ROM patching logic, enhancing the separation of concerns.
- Adjusted CMake configurations to ensure proper compilation of the new core library and updated dependencies in various modules.

Benefits:
- Streamlines the application structure, making it easier to navigate and maintain.
- Enhances code organization by clearly delineating core functionalities from application-specific logic.
- Improves overall architecture by promoting a clearer separation of concerns between different components.
This commit is contained in:
scawful
2025-10-15 20:10:04 -04:00
parent 066ffa46e2
commit d45f7819e1
88 changed files with 393 additions and 290 deletions

View File

@@ -0,0 +1,496 @@
#include "app/service/canvas_automation_service.h"
#ifdef YAZE_WITH_GRPC
#include <grpcpp/grpcpp.h>
#include "protos/canvas_automation.pb.h"
#include "protos/canvas_automation.grpc.pb.h"
#include "app/editor/overworld/overworld_editor.h"
#include "app/gui/canvas/canvas_automation_api.h"
namespace yaze {
namespace {
// Helper to convert absl::Status to grpc::Status
grpc::Status ConvertStatus(const absl::Status& status) {
if (status.ok()) {
return grpc::Status::OK;
}
grpc::StatusCode code;
switch (status.code()) {
case absl::StatusCode::kNotFound:
code = grpc::StatusCode::NOT_FOUND;
break;
case absl::StatusCode::kInvalidArgument:
code = grpc::StatusCode::INVALID_ARGUMENT;
break;
case absl::StatusCode::kFailedPrecondition:
code = grpc::StatusCode::FAILED_PRECONDITION;
break;
case absl::StatusCode::kOutOfRange:
code = grpc::StatusCode::OUT_OF_RANGE;
break;
case absl::StatusCode::kUnimplemented:
code = grpc::StatusCode::UNIMPLEMENTED;
break;
case absl::StatusCode::kInternal:
code = grpc::StatusCode::INTERNAL;
break;
case absl::StatusCode::kUnavailable:
code = grpc::StatusCode::UNAVAILABLE;
break;
default:
code = grpc::StatusCode::UNKNOWN;
break;
}
return grpc::Status(code, std::string(status.message()));
}
} // namespace
void CanvasAutomationServiceImpl::RegisterCanvas(const std::string& canvas_id,
gui::Canvas* canvas) {
canvases_[canvas_id] = canvas;
}
void CanvasAutomationServiceImpl::RegisterOverworldEditor(
const std::string& canvas_id, editor::OverworldEditor* editor) {
overworld_editors_[canvas_id] = editor;
}
gui::Canvas* CanvasAutomationServiceImpl::GetCanvas(const std::string& canvas_id) {
auto it = canvases_.find(canvas_id);
if (it != canvases_.end()) {
return it->second;
}
return nullptr;
}
editor::OverworldEditor* CanvasAutomationServiceImpl::GetOverworldEditor(
const std::string& canvas_id) {
auto it = overworld_editors_.find(canvas_id);
if (it != overworld_editors_.end()) {
return it->second;
}
return nullptr;
}
// ============================================================================
// Tile Operations
// ============================================================================
absl::Status CanvasAutomationServiceImpl::SetTile(
const proto::SetTileRequest* request, proto::SetTileResponse* response) {
auto* canvas = GetCanvas(request->canvas_id());
if (!canvas) {
response->set_success(false);
response->set_error("Canvas not found: " + request->canvas_id());
return absl::NotFoundError(response->error());
}
auto* api = canvas->GetAutomationAPI();
bool success = api->SetTileAt(request->x(), request->y(), request->tile_id());
response->set_success(success);
if (!success) {
response->set_error("Failed to set tile - out of bounds or callback failed");
}
return absl::OkStatus();
}
absl::Status CanvasAutomationServiceImpl::GetTile(
const proto::GetTileRequest* request, proto::GetTileResponse* response) {
auto* canvas = GetCanvas(request->canvas_id());
if (!canvas) {
response->set_success(false);
response->set_error("Canvas not found: " + request->canvas_id());
return absl::NotFoundError(response->error());
}
auto* api = canvas->GetAutomationAPI();
int tile_id = api->GetTileAt(request->x(), request->y());
if (tile_id >= 0) {
response->set_tile_id(tile_id);
response->set_success(true);
} else {
response->set_success(false);
response->set_error("Tile not found - out of bounds or no callback set");
}
return absl::OkStatus();
}
absl::Status CanvasAutomationServiceImpl::SetTiles(
const proto::SetTilesRequest* request, proto::SetTilesResponse* response) {
auto* canvas = GetCanvas(request->canvas_id());
if (!canvas) {
response->set_success(false);
response->set_error("Canvas not found: " + request->canvas_id());
return absl::NotFoundError(response->error());
}
auto* api = canvas->GetAutomationAPI();
std::vector<std::tuple<int, int, int>> tiles;
for (const auto& tile : request->tiles()) {
tiles.push_back({tile.x(), tile.y(), tile.tile_id()});
}
bool success = api->SetTiles(tiles);
response->set_success(success);
response->set_tiles_painted(tiles.size());
return absl::OkStatus();
}
// ============================================================================
// Selection Operations
// ============================================================================
absl::Status CanvasAutomationServiceImpl::SelectTile(
const proto::SelectTileRequest* request, proto::SelectTileResponse* response) {
auto* canvas = GetCanvas(request->canvas_id());
if (!canvas) {
response->set_success(false);
response->set_error("Canvas not found: " + request->canvas_id());
return absl::NotFoundError(response->error());
}
auto* api = canvas->GetAutomationAPI();
api->SelectTile(request->x(), request->y());
response->set_success(true);
return absl::OkStatus();
}
absl::Status CanvasAutomationServiceImpl::SelectTileRect(
const proto::SelectTileRectRequest* request,
proto::SelectTileRectResponse* response) {
auto* canvas = GetCanvas(request->canvas_id());
if (!canvas) {
response->set_success(false);
response->set_error("Canvas not found: " + request->canvas_id());
return absl::NotFoundError(response->error());
}
auto* api = canvas->GetAutomationAPI();
const auto& rect = request->rect();
api->SelectTileRect(rect.x1(), rect.y1(), rect.x2(), rect.y2());
auto selection = api->GetSelection();
response->set_success(true);
response->set_tiles_selected(selection.selected_tiles.size());
return absl::OkStatus();
}
absl::Status CanvasAutomationServiceImpl::GetSelection(
const proto::GetSelectionRequest* request,
proto::GetSelectionResponse* response) {
auto* canvas = GetCanvas(request->canvas_id());
if (!canvas) {
return absl::NotFoundError("Canvas not found: " + request->canvas_id());
}
auto* api = canvas->GetAutomationAPI();
auto selection = api->GetSelection();
response->set_has_selection(selection.has_selection);
for (const auto& tile : selection.selected_tiles) {
auto* coord = response->add_selected_tiles();
coord->set_x(static_cast<int>(tile.x));
coord->set_y(static_cast<int>(tile.y));
}
auto* start = response->mutable_selection_start();
start->set_x(static_cast<int>(selection.selection_start.x));
start->set_y(static_cast<int>(selection.selection_start.y));
auto* end = response->mutable_selection_end();
end->set_x(static_cast<int>(selection.selection_end.x));
end->set_y(static_cast<int>(selection.selection_end.y));
return absl::OkStatus();
}
absl::Status CanvasAutomationServiceImpl::ClearSelection(
const proto::ClearSelectionRequest* request,
proto::ClearSelectionResponse* response) {
auto* canvas = GetCanvas(request->canvas_id());
if (!canvas) {
response->set_success(false);
return absl::NotFoundError("Canvas not found: " + request->canvas_id());
}
auto* api = canvas->GetAutomationAPI();
api->ClearSelection();
response->set_success(true);
return absl::OkStatus();
}
// ============================================================================
// View Operations
// ============================================================================
absl::Status CanvasAutomationServiceImpl::ScrollToTile(
const proto::ScrollToTileRequest* request,
proto::ScrollToTileResponse* response) {
auto* canvas = GetCanvas(request->canvas_id());
if (!canvas) {
response->set_success(false);
response->set_error("Canvas not found: " + request->canvas_id());
return absl::NotFoundError(response->error());
}
auto* api = canvas->GetAutomationAPI();
api->ScrollToTile(request->x(), request->y(), request->center());
response->set_success(true);
return absl::OkStatus();
}
absl::Status CanvasAutomationServiceImpl::CenterOn(
const proto::CenterOnRequest* request, proto::CenterOnResponse* response) {
auto* canvas = GetCanvas(request->canvas_id());
if (!canvas) {
response->set_success(false);
response->set_error("Canvas not found: " + request->canvas_id());
return absl::NotFoundError(response->error());
}
auto* api = canvas->GetAutomationAPI();
api->CenterOn(request->x(), request->y());
response->set_success(true);
return absl::OkStatus();
}
absl::Status CanvasAutomationServiceImpl::SetZoom(
const proto::SetZoomRequest* request, proto::SetZoomResponse* response) {
auto* canvas = GetCanvas(request->canvas_id());
if (!canvas) {
response->set_success(false);
response->set_error("Canvas not found: " + request->canvas_id());
return absl::NotFoundError(response->error());
}
auto* api = canvas->GetAutomationAPI();
api->SetZoom(request->zoom());
float actual_zoom = api->GetZoom();
response->set_success(true);
response->set_actual_zoom(actual_zoom);
return absl::OkStatus();
}
absl::Status CanvasAutomationServiceImpl::GetZoom(
const proto::GetZoomRequest* request, proto::GetZoomResponse* response) {
auto* canvas = GetCanvas(request->canvas_id());
if (!canvas) {
return absl::NotFoundError("Canvas not found: " + request->canvas_id());
}
auto* api = canvas->GetAutomationAPI();
response->set_zoom(api->GetZoom());
return absl::OkStatus();
}
// ============================================================================
// Query Operations
// ============================================================================
absl::Status CanvasAutomationServiceImpl::GetDimensions(
const proto::GetDimensionsRequest* request,
proto::GetDimensionsResponse* response) {
auto* canvas = GetCanvas(request->canvas_id());
if (!canvas) {
return absl::NotFoundError("Canvas not found: " + request->canvas_id());
}
auto* api = canvas->GetAutomationAPI();
auto dims = api->GetDimensions();
auto* proto_dims = response->mutable_dimensions();
proto_dims->set_width_tiles(dims.width_tiles);
proto_dims->set_height_tiles(dims.height_tiles);
proto_dims->set_tile_size(dims.tile_size);
return absl::OkStatus();
}
absl::Status CanvasAutomationServiceImpl::GetVisibleRegion(
const proto::GetVisibleRegionRequest* request,
proto::GetVisibleRegionResponse* response) {
auto* canvas = GetCanvas(request->canvas_id());
if (!canvas) {
return absl::NotFoundError("Canvas not found: " + request->canvas_id());
}
auto* api = canvas->GetAutomationAPI();
auto region = api->GetVisibleRegion();
auto* proto_region = response->mutable_region();
proto_region->set_min_x(region.min_x);
proto_region->set_min_y(region.min_y);
proto_region->set_max_x(region.max_x);
proto_region->set_max_y(region.max_y);
return absl::OkStatus();
}
absl::Status CanvasAutomationServiceImpl::IsTileVisible(
const proto::IsTileVisibleRequest* request,
proto::IsTileVisibleResponse* response) {
auto* canvas = GetCanvas(request->canvas_id());
if (!canvas) {
return absl::NotFoundError("Canvas not found: " + request->canvas_id());
}
auto* api = canvas->GetAutomationAPI();
response->set_is_visible(api->IsTileVisible(request->x(), request->y()));
return absl::OkStatus();
}
// ============================================================================
// gRPC Service Wrapper
// ============================================================================
/**
* @brief gRPC service wrapper that forwards to CanvasAutomationServiceImpl
*
* This adapter implements the proto-generated Service interface and
* forwards all calls to our implementation, converting between gRPC
* and absl::Status types.
*/
class CanvasAutomationServiceGrpc final : public proto::CanvasAutomation::Service {
public:
explicit CanvasAutomationServiceGrpc(CanvasAutomationServiceImpl* impl)
: impl_(impl) {}
// Tile Operations
grpc::Status SetTile(grpc::ServerContext* context,
const proto::SetTileRequest* request,
proto::SetTileResponse* response) override {
return ConvertStatus(impl_->SetTile(request, response));
}
grpc::Status GetTile(grpc::ServerContext* context,
const proto::GetTileRequest* request,
proto::GetTileResponse* response) override {
return ConvertStatus(impl_->GetTile(request, response));
}
grpc::Status SetTiles(grpc::ServerContext* context,
const proto::SetTilesRequest* request,
proto::SetTilesResponse* response) override {
return ConvertStatus(impl_->SetTiles(request, response));
}
// Selection Operations
grpc::Status SelectTile(grpc::ServerContext* context,
const proto::SelectTileRequest* request,
proto::SelectTileResponse* response) override {
return ConvertStatus(impl_->SelectTile(request, response));
}
grpc::Status SelectTileRect(grpc::ServerContext* context,
const proto::SelectTileRectRequest* request,
proto::SelectTileRectResponse* response) override {
return ConvertStatus(impl_->SelectTileRect(request, response));
}
grpc::Status GetSelection(grpc::ServerContext* context,
const proto::GetSelectionRequest* request,
proto::GetSelectionResponse* response) override {
return ConvertStatus(impl_->GetSelection(request, response));
}
grpc::Status ClearSelection(grpc::ServerContext* context,
const proto::ClearSelectionRequest* request,
proto::ClearSelectionResponse* response) override {
return ConvertStatus(impl_->ClearSelection(request, response));
}
// View Operations
grpc::Status ScrollToTile(grpc::ServerContext* context,
const proto::ScrollToTileRequest* request,
proto::ScrollToTileResponse* response) override {
return ConvertStatus(impl_->ScrollToTile(request, response));
}
grpc::Status CenterOn(grpc::ServerContext* context,
const proto::CenterOnRequest* request,
proto::CenterOnResponse* response) override {
return ConvertStatus(impl_->CenterOn(request, response));
}
grpc::Status SetZoom(grpc::ServerContext* context,
const proto::SetZoomRequest* request,
proto::SetZoomResponse* response) override {
return ConvertStatus(impl_->SetZoom(request, response));
}
grpc::Status GetZoom(grpc::ServerContext* context,
const proto::GetZoomRequest* request,
proto::GetZoomResponse* response) override {
return ConvertStatus(impl_->GetZoom(request, response));
}
// Query Operations
grpc::Status GetDimensions(grpc::ServerContext* context,
const proto::GetDimensionsRequest* request,
proto::GetDimensionsResponse* response) override {
return ConvertStatus(impl_->GetDimensions(request, response));
}
grpc::Status GetVisibleRegion(grpc::ServerContext* context,
const proto::GetVisibleRegionRequest* request,
proto::GetVisibleRegionResponse* response) override {
return ConvertStatus(impl_->GetVisibleRegion(request, response));
}
grpc::Status IsTileVisible(grpc::ServerContext* context,
const proto::IsTileVisibleRequest* request,
proto::IsTileVisibleResponse* response) override {
return ConvertStatus(impl_->IsTileVisible(request, response));
}
private:
CanvasAutomationServiceImpl* impl_;
};
// Factory function to create the gRPC wrapper
// Returns as base grpc::Service* to avoid incomplete type issues in headers
std::unique_ptr<grpc::Service> CreateCanvasAutomationServiceGrpc(
CanvasAutomationServiceImpl* impl) {
return std::make_unique<CanvasAutomationServiceGrpc>(impl);
}
} // namespace yaze
#endif // YAZE_WITH_GRPC

View File

@@ -0,0 +1,147 @@
#ifndef YAZE_APP_CORE_SERVICE_CANVAS_AUTOMATION_SERVICE_H_
#define YAZE_APP_CORE_SERVICE_CANVAS_AUTOMATION_SERVICE_H_
#include "grpcpp/impl/service_type.h"
#ifdef YAZE_WITH_GRPC
#include <memory>
#include <string>
#include <unordered_map>
#include "absl/status/status.h"
#include "app/gui/canvas/canvas.h"
// Forward declarations
namespace grpc {
class ServerContext;
}
namespace yaze {
namespace editor {
class OverworldEditor;
}
namespace proto {
// Forward declare proto types
class SetTileRequest;
class SetTileResponse;
class GetTileRequest;
class GetTileResponse;
class SetTilesRequest;
class SetTilesResponse;
class SelectTileRequest;
class SelectTileResponse;
class SelectTileRectRequest;
class SelectTileRectResponse;
class GetSelectionRequest;
class GetSelectionResponse;
class ClearSelectionRequest;
class ClearSelectionResponse;
class ScrollToTileRequest;
class ScrollToTileResponse;
class CenterOnRequest;
class CenterOnResponse;
class SetZoomRequest;
class SetZoomResponse;
class GetZoomRequest;
class GetZoomResponse;
class GetDimensionsRequest;
class GetDimensionsResponse;
class GetVisibleRegionRequest;
class GetVisibleRegionResponse;
class IsTileVisibleRequest;
class IsTileVisibleResponse;
} // namespace proto
/**
* @brief Implementation of CanvasAutomation gRPC service
*
* Provides remote access to canvas automation API for:
* - AI agent tool calls
* - Remote GUI testing
* - Collaborative editing workflows
* - CLI automation scripts
*/
class CanvasAutomationServiceImpl {
public:
CanvasAutomationServiceImpl() = default;
// Register a canvas for automation
void RegisterCanvas(const std::string& canvas_id, gui::Canvas* canvas);
// Register an overworld editor (for tile get/set callbacks)
void RegisterOverworldEditor(const std::string& canvas_id,
editor::OverworldEditor* editor);
// RPC method implementations
absl::Status SetTile(const proto::SetTileRequest* request,
proto::SetTileResponse* response);
absl::Status GetTile(const proto::GetTileRequest* request,
proto::GetTileResponse* response);
absl::Status SetTiles(const proto::SetTilesRequest* request,
proto::SetTilesResponse* response);
absl::Status SelectTile(const proto::SelectTileRequest* request,
proto::SelectTileResponse* response);
absl::Status SelectTileRect(const proto::SelectTileRectRequest* request,
proto::SelectTileRectResponse* response);
absl::Status GetSelection(const proto::GetSelectionRequest* request,
proto::GetSelectionResponse* response);
absl::Status ClearSelection(const proto::ClearSelectionRequest* request,
proto::ClearSelectionResponse* response);
absl::Status ScrollToTile(const proto::ScrollToTileRequest* request,
proto::ScrollToTileResponse* response);
absl::Status CenterOn(const proto::CenterOnRequest* request,
proto::CenterOnResponse* response);
absl::Status SetZoom(const proto::SetZoomRequest* request,
proto::SetZoomResponse* response);
absl::Status GetZoom(const proto::GetZoomRequest* request,
proto::GetZoomResponse* response);
absl::Status GetDimensions(const proto::GetDimensionsRequest* request,
proto::GetDimensionsResponse* response);
absl::Status GetVisibleRegion(const proto::GetVisibleRegionRequest* request,
proto::GetVisibleRegionResponse* response);
absl::Status IsTileVisible(const proto::IsTileVisibleRequest* request,
proto::IsTileVisibleResponse* response);
private:
gui::Canvas* GetCanvas(const std::string& canvas_id);
editor::OverworldEditor* GetOverworldEditor(const std::string& canvas_id);
// Canvas registry
std::unordered_map<std::string, gui::Canvas*> canvases_;
// Editor registry (for tile callbacks)
std::unordered_map<std::string, editor::OverworldEditor*> overworld_editors_;
};
/**
* @brief Factory function to create gRPC service wrapper
*
* Creates the gRPC service wrapper for CanvasAutomationServiceImpl.
* The wrapper handles the conversion between gRPC and absl::Status.
* Returns as base grpc::Service to avoid incomplete type issues.
*
* @param impl Pointer to implementation (not owned)
* @return Unique pointer to gRPC service
*/
std::unique_ptr<grpc::Service> CreateCanvasAutomationServiceGrpc(
CanvasAutomationServiceImpl* impl);
} // namespace yaze
#endif // YAZE_WITH_GRPC
#endif // YAZE_APP_CORE_SERVICE_CANVAS_AUTOMATION_SERVICE_H_

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,166 @@
#ifndef YAZE_APP_CORE_SERVICE_IMGUI_TEST_HARNESS_SERVICE_H_
#define YAZE_APP_CORE_SERVICE_IMGUI_TEST_HARNESS_SERVICE_H_
#ifdef YAZE_WITH_GRPC
#include <memory>
#include <string>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "app/service/widget_discovery_service.h"
#include "app/test/test_recorder.h"
// Undefine Windows macros that conflict with protobuf generated code
#ifdef _WIN32
#ifdef DWORD
#undef DWORD
#endif
#ifdef ERROR
#undef ERROR
#endif
#endif // _WIN32
// Include grpcpp headers for unique_ptr<Server> in member variable
#include <grpcpp/server.h>
// Forward declarations to avoid including gRPC headers in public interface
namespace grpc {
class ServerContext;
} // namespace grpc
namespace yaze {
namespace test {
// Forward declare TestManager
class TestManager;
// Forward declare proto types
class PingRequest;
class PingResponse;
class ClickRequest;
class ClickResponse;
class TypeRequest;
class TypeResponse;
class WaitRequest;
class WaitResponse;
class AssertRequest;
class AssertResponse;
class ScreenshotRequest;
class ScreenshotResponse;
class GetTestStatusRequest;
class GetTestStatusResponse;
class ListTestsRequest;
class ListTestsResponse;
class GetTestResultsRequest;
class GetTestResultsResponse;
class DiscoverWidgetsRequest;
class DiscoverWidgetsResponse;
class StartRecordingRequest;
class StartRecordingResponse;
class StopRecordingRequest;
class StopRecordingResponse;
class ReplayTestRequest;
class ReplayTestResponse;
// Implementation of ImGuiTestHarness gRPC service
// This class provides the actual RPC handlers for automated GUI testing
class ImGuiTestHarnessServiceImpl {
public:
// Constructor now takes TestManager reference for ImGuiTestEngine access
explicit ImGuiTestHarnessServiceImpl(TestManager* test_manager)
: test_manager_(test_manager), test_recorder_(test_manager) {}
~ImGuiTestHarnessServiceImpl() = default;
// Disable copy and move
ImGuiTestHarnessServiceImpl(const ImGuiTestHarnessServiceImpl&) = delete;
ImGuiTestHarnessServiceImpl& operator=(const ImGuiTestHarnessServiceImpl&) =
delete;
// RPC Handlers - implemented in imgui_test_harness_service.cc
// Health check - verifies the service is running
absl::Status Ping(const PingRequest* request, PingResponse* response);
// Click a button or interactive element
absl::Status Click(const ClickRequest* request, ClickResponse* response);
// Type text into an input field
absl::Status Type(const TypeRequest* request, TypeResponse* response);
// Wait for a condition to be true
absl::Status Wait(const WaitRequest* request, WaitResponse* response);
// Assert that a condition is true
absl::Status Assert(const AssertRequest* request, AssertResponse* response);
// Capture a screenshot
absl::Status Screenshot(const ScreenshotRequest* request,
ScreenshotResponse* response);
// Test introspection APIs
absl::Status GetTestStatus(const GetTestStatusRequest* request,
GetTestStatusResponse* response);
absl::Status ListTests(const ListTestsRequest* request,
ListTestsResponse* response);
absl::Status GetTestResults(const GetTestResultsRequest* request,
GetTestResultsResponse* response);
absl::Status DiscoverWidgets(const DiscoverWidgetsRequest* request,
DiscoverWidgetsResponse* response);
absl::Status StartRecording(const StartRecordingRequest* request,
StartRecordingResponse* response);
absl::Status StopRecording(const StopRecordingRequest* request,
StopRecordingResponse* response);
absl::Status ReplayTest(const ReplayTestRequest* request,
ReplayTestResponse* response);
private:
TestManager* test_manager_; // Non-owning pointer to access ImGuiTestEngine
WidgetDiscoveryService widget_discovery_service_;
TestRecorder test_recorder_;
};
// Forward declaration of the gRPC service wrapper
class ImGuiTestHarnessServiceGrpc;
// Singleton server managing the gRPC service
// This class manages the lifecycle of the gRPC server
class ImGuiTestHarnessServer {
public:
// Get the singleton instance
static ImGuiTestHarnessServer& Instance();
// Start the gRPC server on the specified port
// @param port The port to listen on (default 50051)
// @param test_manager Pointer to TestManager for ImGuiTestEngine access
// @return OK status if server started successfully, error otherwise
absl::Status Start(int port, TestManager* test_manager);
// Shutdown the server gracefully
void Shutdown();
// Check if the server is currently running
bool IsRunning() const { return server_ != nullptr; }
// Get the port the server is listening on (0 if not running)
int Port() const { return port_; }
private:
ImGuiTestHarnessServer() = default;
~ImGuiTestHarnessServer(); // Defined in .cc file to allow incomplete type deletion
// Disable copy and move
ImGuiTestHarnessServer(const ImGuiTestHarnessServer&) = delete;
ImGuiTestHarnessServer& operator=(const ImGuiTestHarnessServer&) = delete;
std::unique_ptr<grpc::Server> server_;
std::unique_ptr<ImGuiTestHarnessServiceImpl> service_;
std::unique_ptr<ImGuiTestHarnessServiceGrpc> grpc_service_;
int port_ = 0;
};
} // namespace test
} // namespace yaze
#endif // YAZE_WITH_GRPC
#endif // YAZE_APP_CORE_SERVICE_IMGUI_TEST_HARNESS_SERVICE_H_

View File

@@ -0,0 +1,264 @@
#include "app/service/screenshot_utils.h"
#ifdef YAZE_WITH_GRPC
#include <SDL.h>
// Undefine Windows macros that conflict with protobuf generated code
// SDL.h includes Windows.h on Windows, which defines these macros
#ifdef _WIN32
#ifdef DWORD
#undef DWORD
#endif
#ifdef ERROR
#undef ERROR
#endif
#ifdef OVERFLOW
#undef OVERFLOW
#endif
#ifdef IGNORE
#undef IGNORE
#endif
#endif // _WIN32
#include <filesystem>
#include <string>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_format.h"
#include "absl/time/clock.h"
#include "imgui.h"
#include "imgui_internal.h"
namespace yaze {
namespace test {
namespace {
struct ImGui_ImplSDLRenderer2_Data {
SDL_Renderer* Renderer;
};
std::filesystem::path DefaultScreenshotPath() {
std::filesystem::path base_dir =
std::filesystem::temp_directory_path() / "yaze" / "test-results";
std::error_code ec;
std::filesystem::create_directories(base_dir, ec);
const int64_t timestamp_ms = absl::ToUnixMillis(absl::Now());
return base_dir /
std::filesystem::path(
absl::StrFormat("harness_%lld.bmp", static_cast<long long>(timestamp_ms)));
}
} // namespace
absl::StatusOr<ScreenshotArtifact> CaptureHarnessScreenshot(
const std::string& preferred_path) {
ImGuiIO& io = ImGui::GetIO();
auto* backend_data =
static_cast<ImGui_ImplSDLRenderer2_Data*>(io.BackendRendererUserData);
if (!backend_data || !backend_data->Renderer) {
return absl::FailedPreconditionError("SDL renderer not available");
}
SDL_Renderer* renderer = backend_data->Renderer;
int width = 0;
int height = 0;
if (SDL_GetRendererOutputSize(renderer, &width, &height) != 0) {
return absl::InternalError(
absl::StrFormat("Failed to get renderer size: %s", SDL_GetError()));
}
std::filesystem::path output_path = preferred_path.empty()
? DefaultScreenshotPath()
: std::filesystem::path(preferred_path);
if (output_path.has_parent_path()) {
std::error_code ec;
std::filesystem::create_directories(output_path.parent_path(), ec);
}
SDL_Surface* surface = SDL_CreateRGBSurface(0, width, height, 32, 0x00FF0000,
0x0000FF00, 0x000000FF,
0xFF000000);
if (!surface) {
return absl::InternalError(
absl::StrFormat("Failed to create SDL surface: %s", SDL_GetError()));
}
if (SDL_RenderReadPixels(renderer, nullptr, SDL_PIXELFORMAT_ARGB8888,
surface->pixels, surface->pitch) != 0) {
SDL_FreeSurface(surface);
return absl::InternalError(
absl::StrFormat("Failed to read renderer pixels: %s", SDL_GetError()));
}
if (SDL_SaveBMP(surface, output_path.string().c_str()) != 0) {
SDL_FreeSurface(surface);
return absl::InternalError(
absl::StrFormat("Failed to save BMP: %s", SDL_GetError()));
}
SDL_FreeSurface(surface);
std::error_code ec;
const int64_t file_size =
std::filesystem::file_size(output_path, ec);
if (ec) {
return absl::InternalError(
absl::StrFormat("Failed to stat screenshot %s: %s",
output_path.string(), ec.message()));
}
ScreenshotArtifact artifact;
artifact.file_path = output_path.string();
artifact.width = width;
artifact.height = height;
artifact.file_size_bytes = file_size;
return artifact;
}
absl::StatusOr<ScreenshotArtifact> CaptureHarnessScreenshotRegion(
const std::optional<CaptureRegion>& region,
const std::string& preferred_path) {
ImGuiIO& io = ImGui::GetIO();
auto* backend_data =
static_cast<ImGui_ImplSDLRenderer2_Data*>(io.BackendRendererUserData);
if (!backend_data || !backend_data->Renderer) {
return absl::FailedPreconditionError("SDL renderer not available");
}
SDL_Renderer* renderer = backend_data->Renderer;
// Get full renderer size
int full_width = 0;
int full_height = 0;
if (SDL_GetRendererOutputSize(renderer, &full_width, &full_height) != 0) {
return absl::InternalError(
absl::StrFormat("Failed to get renderer size: %s", SDL_GetError()));
}
// Determine capture region
int capture_x = 0;
int capture_y = 0;
int capture_width = full_width;
int capture_height = full_height;
if (region.has_value()) {
capture_x = region->x;
capture_y = region->y;
capture_width = region->width;
capture_height = region->height;
// Clamp to renderer bounds
if (capture_x < 0) capture_x = 0;
if (capture_y < 0) capture_y = 0;
if (capture_x + capture_width > full_width) {
capture_width = full_width - capture_x;
}
if (capture_y + capture_height > full_height) {
capture_height = full_height - capture_y;
}
if (capture_width <= 0 || capture_height <= 0) {
return absl::InvalidArgumentError("Invalid capture region");
}
}
std::filesystem::path output_path = preferred_path.empty()
? DefaultScreenshotPath()
: std::filesystem::path(preferred_path);
if (output_path.has_parent_path()) {
std::error_code ec;
std::filesystem::create_directories(output_path.parent_path(), ec);
}
// Create surface for the capture region
SDL_Surface* surface = SDL_CreateRGBSurface(0, capture_width, capture_height,
32, 0x00FF0000, 0x0000FF00,
0x000000FF, 0xFF000000);
if (!surface) {
return absl::InternalError(
absl::StrFormat("Failed to create SDL surface: %s", SDL_GetError()));
}
// Read pixels from the specified region
SDL_Rect region_rect = {capture_x, capture_y, capture_width, capture_height};
if (SDL_RenderReadPixels(renderer, &region_rect, SDL_PIXELFORMAT_ARGB8888,
surface->pixels, surface->pitch) != 0) {
SDL_FreeSurface(surface);
return absl::InternalError(
absl::StrFormat("Failed to read renderer pixels: %s", SDL_GetError()));
}
if (SDL_SaveBMP(surface, output_path.string().c_str()) != 0) {
SDL_FreeSurface(surface);
return absl::InternalError(
absl::StrFormat("Failed to save BMP: %s", SDL_GetError()));
}
SDL_FreeSurface(surface);
std::error_code ec;
const int64_t file_size = std::filesystem::file_size(output_path, ec);
if (ec) {
return absl::InternalError(
absl::StrFormat("Failed to stat screenshot %s: %s",
output_path.string(), ec.message()));
}
ScreenshotArtifact artifact;
artifact.file_path = output_path.string();
artifact.width = capture_width;
artifact.height = capture_height;
artifact.file_size_bytes = file_size;
return artifact;
}
absl::StatusOr<ScreenshotArtifact> CaptureActiveWindow(
const std::string& preferred_path) {
ImGuiContext* ctx = ImGui::GetCurrentContext();
if (!ctx || !ctx->NavWindow) {
return absl::FailedPreconditionError("No active ImGui window");
}
ImGuiWindow* window = ctx->NavWindow;
CaptureRegion region;
region.x = static_cast<int>(window->Pos.x);
region.y = static_cast<int>(window->Pos.y);
region.width = static_cast<int>(window->Size.x);
region.height = static_cast<int>(window->Size.y);
return CaptureHarnessScreenshotRegion(region, preferred_path);
}
absl::StatusOr<ScreenshotArtifact> CaptureWindowByName(
const std::string& window_name,
const std::string& preferred_path) {
ImGuiContext* ctx = ImGui::GetCurrentContext();
if (!ctx) {
return absl::FailedPreconditionError("No ImGui context");
}
ImGuiWindow* window = ImGui::FindWindowByName(window_name.c_str());
if (!window) {
return absl::NotFoundError(
absl::StrFormat("Window '%s' not found", window_name));
}
CaptureRegion region;
region.x = static_cast<int>(window->Pos.x);
region.y = static_cast<int>(window->Pos.y);
region.width = static_cast<int>(window->Size.x);
region.height = static_cast<int>(window->Size.y);
return CaptureHarnessScreenshotRegion(region, preferred_path);
}
} // namespace test
} // namespace yaze
#endif // YAZE_WITH_GRPC

View File

@@ -0,0 +1,54 @@
#ifndef YAZE_APP_CORE_SERVICE_SCREENSHOT_UTILS_H_
#define YAZE_APP_CORE_SERVICE_SCREENSHOT_UTILS_H_
#ifdef YAZE_WITH_GRPC
#include <optional>
#include <string>
#include "absl/status/statusor.h"
namespace yaze {
namespace test {
struct ScreenshotArtifact {
std::string file_path;
int width = 0;
int height = 0;
int64_t file_size_bytes = 0;
};
struct CaptureRegion {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
};
// Captures the current renderer output into a BMP file.
// If preferred_path is empty, an appropriate path under the system temp
// directory is generated automatically. Returns the resolved artifact metadata
// on success.
absl::StatusOr<ScreenshotArtifact> CaptureHarnessScreenshot(
const std::string& preferred_path = "");
// Captures a specific region of the renderer output.
// If region is nullopt, captures the full renderer (same as CaptureHarnessScreenshot).
absl::StatusOr<ScreenshotArtifact> CaptureHarnessScreenshotRegion(
const std::optional<CaptureRegion>& region,
const std::string& preferred_path = "");
// Captures the currently active ImGui window.
absl::StatusOr<ScreenshotArtifact> CaptureActiveWindow(
const std::string& preferred_path = "");
// Captures a specific ImGui window by name.
absl::StatusOr<ScreenshotArtifact> CaptureWindowByName(
const std::string& window_name,
const std::string& preferred_path = "");
} // namespace test
} // namespace yaze
#endif // YAZE_WITH_GRPC
#endif // YAZE_APP_CORE_SERVICE_SCREENSHOT_UTILS_H_

View File

@@ -0,0 +1,183 @@
#include "app/service/unified_grpc_server.h"
#ifdef YAZE_WITH_GRPC
#include <iostream>
#include <thread>
#include "absl/strings/str_format.h"
#include "app/service/imgui_test_harness_service.h"
#include "app/service/canvas_automation_service.h"
#include "app/net/rom_service_impl.h"
#include "app/rom.h"
#include <grpcpp/grpcpp.h>
#include "protos/canvas_automation.grpc.pb.h"
namespace yaze {
YazeGRPCServer::YazeGRPCServer()
: is_running_(false) {
}
// Destructor defined here so CanvasAutomationServiceGrpc is a complete type
YazeGRPCServer::~YazeGRPCServer() {
Shutdown();
}
absl::Status YazeGRPCServer::Initialize(
int port,
test::TestManager* test_manager,
Rom* rom,
net::RomVersionManager* version_mgr,
net::ProposalApprovalManager* approval_mgr,
CanvasAutomationServiceImpl* canvas_service) {
if (is_running_) {
return absl::FailedPreconditionError("Server is already running");
}
config_.port = port;
// Create ImGuiTestHarness service if test_manager provided
if (config_.enable_test_harness && test_manager) {
test_harness_service_ =
std::make_unique<test::ImGuiTestHarnessServiceImpl>(test_manager);
std::cout << "✓ ImGuiTestHarness service initialized\n";
} else if (config_.enable_test_harness) {
std::cout << "⚠ ImGuiTestHarness requested but no TestManager provided\n";
}
// Create ROM service if rom provided
if (config_.enable_rom_service && rom) {
rom_service_ = std::make_unique<net::RomServiceImpl>(
rom, version_mgr, approval_mgr);
// Configure ROM service
net::RomServiceImpl::Config rom_config;
rom_config.require_approval_for_writes = config_.require_approval_for_rom_writes;
rom_service_->SetConfig(rom_config);
std::cout << "✓ ROM service initialized\n";
} else if (config_.enable_rom_service) {
std::cout << "⚠ ROM service requested but no ROM provided\n";
}
// Create Canvas Automation service if canvas_service provided
if (config_.enable_canvas_automation && canvas_service) {
// Store the provided service (not owned by us)
canvas_service_ = std::unique_ptr<CanvasAutomationServiceImpl>(canvas_service);
std::cout << "✓ Canvas Automation service initialized\n";
} else if (config_.enable_canvas_automation) {
std::cout << "⚠ Canvas Automation requested but no service provided\n";
}
if (!test_harness_service_ && !rom_service_ && !canvas_service_) {
return absl::InvalidArgumentError(
"At least one service must be enabled and initialized");
}
return absl::OkStatus();
}
absl::Status YazeGRPCServer::Start() {
auto status = BuildServer();
if (!status.ok()) {
return status;
}
std::cout << "✓ YAZE gRPC automation server listening on 0.0.0.0:" << config_.port << "\n";
if (test_harness_service_) {
std::cout << " ✓ ImGuiTestHarness available\n";
}
if (rom_service_) {
std::cout << " ✓ ROM service available\n";
}
if (canvas_service_) {
std::cout << " ✓ Canvas Automation available\n";
}
std::cout << "\nServer is ready to accept requests...\n";
// Block until server is shut down
server_->Wait();
return absl::OkStatus();
}
absl::Status YazeGRPCServer::StartAsync() {
auto status = BuildServer();
if (!status.ok()) {
return status;
}
std::cout << "✓ Unified gRPC server started on port " << config_.port << "\n";
// Server runs in background, doesn't block
return absl::OkStatus();
}
void YazeGRPCServer::Shutdown() {
if (server_ && is_running_) {
std::cout << "⏹ Shutting down unified gRPC server...\n";
server_->Shutdown();
server_.reset();
is_running_ = false;
std::cout << "✓ Server stopped\n";
}
}
bool YazeGRPCServer::IsRunning() const {
return is_running_;
}
absl::Status YazeGRPCServer::BuildServer() {
if (is_running_) {
return absl::FailedPreconditionError("Server already running");
}
std::string server_address = absl::StrFormat("0.0.0.0:%d", config_.port);
grpc::ServerBuilder builder;
// Listen on all interfaces
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
// Register services
if (test_harness_service_) {
// Note: The actual registration requires the gRPC service wrapper
// This is a simplified version - full implementation would need
// the wrapper from imgui_test_harness_service.cc
std::cout << " Registering ImGuiTestHarness service...\n";
// builder.RegisterService(test_harness_grpc_wrapper_.get());
}
if (rom_service_) {
std::cout << " Registering ROM service...\n";
builder.RegisterService(rom_service_.get());
}
if (canvas_service_) {
std::cout << " Registering Canvas Automation service...\n";
// Create gRPC wrapper using factory function
canvas_grpc_service_ = CreateCanvasAutomationServiceGrpc(canvas_service_.get());
builder.RegisterService(canvas_grpc_service_.get());
}
// Build and start
server_ = builder.BuildAndStart();
if (!server_) {
return absl::InternalError(
absl::StrFormat("Failed to start server on %s", server_address));
}
is_running_ = true;
return absl::OkStatus();
}
} // namespace yaze
#endif // YAZE_WITH_GRPC

View File

@@ -0,0 +1,149 @@
#ifndef YAZE_APP_CORE_SERVICE_UNIFIED_GRPC_SERVER_H_
#define YAZE_APP_CORE_SERVICE_UNIFIED_GRPC_SERVER_H_
#ifdef YAZE_WITH_GRPC
#include <memory>
#include "absl/status/status.h"
#include "app/net/rom_version_manager.h"
// Include grpcpp for grpc::Service forward declaration
#include <grpcpp/impl/service_type.h>
namespace grpc {
class Server;
}
namespace yaze {
// Forward declarations
class CanvasAutomationServiceImpl;
class Rom;
namespace net {
class ProposalApprovalManager;
class RomServiceImpl;
}
namespace test {
class TestManager;
class ImGuiTestHarnessServiceImpl;
}
/**
* @class YazeGRPCServer
* @brief YAZE's unified gRPC server for Zelda3 editor automation
*
* This server combines multiple automation services for the Zelda editor:
* 1. ImGuiTestHarness - GUI test automation (widget discovery, screenshots, etc.)
* 2. RomService - ROM manipulation (read/write, proposals, version management)
* 3. CanvasAutomation - Canvas operations (tiles, selection, zoom, pan)
*
* All services share the same gRPC server instance and port, allowing
* clients (CLI, AI agents, remote scripts) to interact with GUI, ROM data,
* and canvas operations simultaneously.
*
* Example usage:
* ```cpp
* YazeGRPCServer server;
* server.Initialize(50051, test_manager, rom, version_mgr, approval_mgr, canvas_service);
* server.Start();
* // ... do work ...
* server.Shutdown();
* ```
*/
class YazeGRPCServer {
public:
/**
* @brief Configuration for the unified server
*/
struct Config {
int port = 50051;
bool enable_test_harness = true;
bool enable_rom_service = true;
bool enable_canvas_automation = true;
bool require_approval_for_rom_writes = true;
};
YazeGRPCServer();
// Destructor must be defined in .cc file to allow deletion of incomplete types
~YazeGRPCServer();
/**
* @brief Initialize the server with all required services
* @param port Port to listen on (default 50051)
* @param test_manager TestManager for GUI automation (optional)
* @param rom ROM instance for ROM service (optional)
* @param version_mgr Version manager for ROM snapshots (optional)
* @param approval_mgr Approval manager for proposals (optional)
* @param canvas_service Canvas automation service implementation (optional)
* @return OK status if initialized successfully
*/
absl::Status Initialize(
int port,
test::TestManager* test_manager = nullptr,
Rom* rom = nullptr,
net::RomVersionManager* version_mgr = nullptr,
net::ProposalApprovalManager* approval_mgr = nullptr,
CanvasAutomationServiceImpl* canvas_service = nullptr);
/**
* @brief Start the gRPC server (blocking)
* Starts the server and blocks until Shutdown() is called
*/
absl::Status Start();
/**
* @brief Start the server in a background thread (non-blocking)
* Returns immediately after starting the server
*/
absl::Status StartAsync();
/**
* @brief Shutdown the server gracefully
*/
void Shutdown();
/**
* @brief Check if server is currently running
*/
bool IsRunning() const;
/**
* @brief Get the port the server is listening on
*/
int Port() const { return config_.port; }
/**
* @brief Update configuration (must be called before Start)
*/
void SetConfig(const Config& config) { config_ = config; }
private:
Config config_;
std::unique_ptr<grpc::Server> server_;
std::unique_ptr<test::ImGuiTestHarnessServiceImpl> test_harness_service_;
std::unique_ptr<net::RomServiceImpl> rom_service_;
std::unique_ptr<CanvasAutomationServiceImpl> canvas_service_;
// Store as base grpc::Service* to avoid incomplete type issues
std::unique_ptr<grpc::Service> canvas_grpc_service_;
bool is_running_;
// Build the gRPC server with all services
absl::Status BuildServer();
};
} // namespace yaze
#endif // YAZE_WITH_GRPC
#endif // YAZE_APP_CORE_SERVICE_UNIFIED_GRPC_SERVER_H_
// Backwards compatibility alias
#ifdef YAZE_WITH_GRPC
namespace yaze {
using UnifiedGRPCServer = YazeGRPCServer;
}
#endif

View File

@@ -0,0 +1,259 @@
#include "app/service/widget_discovery_service.h"
#include <algorithm>
#include <cstdint>
#include <map>
#include <string>
#include <utility>
#include "absl/strings/ascii.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
namespace yaze {
namespace test {
namespace {
struct WindowEntry {
int index = -1;
bool visible = false;
};
} // namespace
void WidgetDiscoveryService::CollectWidgets(
ImGuiTestContext* ctx, const DiscoverWidgetsRequest& request,
DiscoverWidgetsResponse* response) const {
if (!response) {
return;
}
response->clear_windows();
response->set_total_widgets(0);
response->set_generated_at_ms(absl::ToUnixMillis(absl::Now()));
const auto& registry = gui::WidgetIdRegistry::Instance().GetAllWidgets();
if (registry.empty()) {
return;
}
const std::string window_filter_lower =
absl::AsciiStrToLower(std::string(request.window_filter()));
const std::string path_prefix_lower =
absl::AsciiStrToLower(std::string(request.path_prefix()));
const bool include_invisible = request.include_invisible();
const bool include_disabled = request.include_disabled();
std::map<std::string, WindowEntry> window_lookup;
int total_widgets = 0;
for (const auto& [path, info] : registry) {
if (!MatchesType(info.type, request.type_filter())) {
continue;
}
if (!MatchesPathPrefix(path, path_prefix_lower)) {
continue;
}
const std::string window_name =
info.window_name.empty() ? ExtractWindowName(path) : info.window_name;
if (!MatchesWindow(window_name, window_filter_lower)) {
continue;
}
const std::string label =
info.label.empty() ? ExtractLabel(path) : info.label;
bool widget_enabled = info.enabled;
bool widget_visible = info.visible;
#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
bool has_item_info = false;
ImGuiTestItemInfo item_info;
if (ctx) {
item_info = ctx->ItemInfo(label.c_str(), ImGuiTestOpFlags_NoError);
if (item_info.ID != 0) {
has_item_info = true;
widget_visible = item_info.RectClipped.GetWidth() > 0.0f &&
item_info.RectClipped.GetHeight() > 0.0f;
widget_enabled = (item_info.ItemFlags & ImGuiItemFlags_Disabled) == 0;
}
}
#else
(void)ctx;
#endif
auto [it, inserted] = window_lookup.emplace(window_name, WindowEntry{});
WindowEntry& entry = it->second;
if (inserted) {
entry.visible = widget_visible;
} else {
entry.visible = entry.visible || widget_visible;
}
if (!include_invisible && !widget_visible) {
continue;
}
if (!include_disabled && !widget_enabled) {
continue;
}
if (entry.index == -1) {
DiscoveredWindow* window_proto = response->add_windows();
entry.index = response->windows_size() - 1;
window_proto->set_name(window_name);
window_proto->set_visible(entry.visible);
}
auto* window_proto = response->mutable_windows(entry.index);
window_proto->set_visible(entry.visible);
auto* widget_proto = window_proto->add_widgets();
widget_proto->set_path(path);
widget_proto->set_label(label);
widget_proto->set_type(info.type);
widget_proto->set_suggested_action(SuggestedAction(info.type, label));
widget_proto->set_visible(widget_visible);
widget_proto->set_enabled(widget_enabled);
widget_proto->set_widget_id(info.imgui_id);
if (!info.description.empty()) {
widget_proto->set_description(info.description);
}
if (info.bounds.valid) {
WidgetBounds* bounds = widget_proto->mutable_bounds();
bounds->set_min_x(info.bounds.min_x);
bounds->set_min_y(info.bounds.min_y);
bounds->set_max_x(info.bounds.max_x);
bounds->set_max_y(info.bounds.max_y);
#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
} else if (ctx && has_item_info) {
WidgetBounds* bounds = widget_proto->mutable_bounds();
bounds->set_min_x(item_info.RectFull.Min.x);
bounds->set_min_y(item_info.RectFull.Min.y);
bounds->set_max_x(item_info.RectFull.Max.x);
bounds->set_max_y(item_info.RectFull.Max.y);
} else {
(void)ctx;
#else
} else {
(void)ctx;
#endif
}
widget_proto->set_last_seen_frame(info.last_seen_frame);
int64_t last_seen_ms = 0;
if (info.last_seen_time != absl::Time()) {
last_seen_ms = absl::ToUnixMillis(info.last_seen_time);
}
widget_proto->set_last_seen_at_ms(last_seen_ms);
widget_proto->set_stale(info.stale_frame_count > 0);
++total_widgets;
}
response->set_total_widgets(total_widgets);
}
bool WidgetDiscoveryService::MatchesWindow(absl::string_view window_name,
absl::string_view filter_lower) const {
if (filter_lower.empty()) {
return true;
}
std::string name_lower = absl::AsciiStrToLower(std::string(window_name));
return absl::StrContains(name_lower, filter_lower);
}
bool WidgetDiscoveryService::MatchesPathPrefix(absl::string_view path,
absl::string_view prefix_lower) const {
if (prefix_lower.empty()) {
return true;
}
std::string path_lower = absl::AsciiStrToLower(std::string(path));
return absl::StartsWith(path_lower, prefix_lower);
}
bool WidgetDiscoveryService::MatchesType(absl::string_view type,
WidgetType filter) const {
if (filter == WIDGET_TYPE_UNSPECIFIED || filter == WIDGET_TYPE_ALL) {
return true;
}
std::string type_lower = absl::AsciiStrToLower(std::string(type));
switch (filter) {
case WIDGET_TYPE_BUTTON:
return type_lower == "button" || type_lower == "menuitem";
case WIDGET_TYPE_INPUT:
return type_lower == "input" || type_lower == "textbox" ||
type_lower == "inputtext";
case WIDGET_TYPE_MENU:
return type_lower == "menu" || type_lower == "menuitem";
case WIDGET_TYPE_TAB:
return type_lower == "tab";
case WIDGET_TYPE_CHECKBOX:
return type_lower == "checkbox";
case WIDGET_TYPE_SLIDER:
return type_lower == "slider" || type_lower == "drag";
case WIDGET_TYPE_CANVAS:
return type_lower == "canvas";
case WIDGET_TYPE_SELECTABLE:
return type_lower == "selectable";
case WIDGET_TYPE_OTHER:
return true;
default:
return true;
}
}
std::string WidgetDiscoveryService::ExtractWindowName(
absl::string_view path) const {
size_t slash = path.find('/');
if (slash == absl::string_view::npos) {
return std::string(path);
}
return std::string(path.substr(0, slash));
}
std::string WidgetDiscoveryService::ExtractLabel(absl::string_view path) const {
size_t colon = path.rfind(':');
if (colon == absl::string_view::npos) {
size_t slash = path.rfind('/');
if (slash == absl::string_view::npos) {
return std::string(path);
}
return std::string(path.substr(slash + 1));
}
return std::string(path.substr(colon + 1));
}
std::string WidgetDiscoveryService::SuggestedAction(absl::string_view type,
absl::string_view label) const {
std::string type_lower = absl::AsciiStrToLower(std::string(type));
if (type_lower == "button" || type_lower == "menuitem") {
return absl::StrCat("Click button:", label);
}
if (type_lower == "checkbox") {
return absl::StrCat("Toggle checkbox:", label);
}
if (type_lower == "slider" || type_lower == "drag") {
return absl::StrCat("Adjust slider:", label);
}
if (type_lower == "input" || type_lower == "textbox" ||
type_lower == "inputtext") {
return absl::StrCat("Type text into:", label);
}
if (type_lower == "canvas") {
return absl::StrCat("Interact with canvas:", label);
}
if (type_lower == "tab") {
return absl::StrCat("Switch to tab:", label);
}
return absl::StrCat("Interact with:", label);
}
} // namespace test
} // namespace yaze

View File

@@ -0,0 +1,57 @@
#ifndef YAZE_APP_CORE_SERVICE_WIDGET_DISCOVERY_SERVICE_H_
#define YAZE_APP_CORE_SERVICE_WIDGET_DISCOVERY_SERVICE_H_
#include <string>
#include <vector>
#include "absl/strings/string_view.h"
#ifdef _WIN32
#pragma push_macro("DWORD")
#pragma push_macro("ERROR")
#undef DWORD
#undef ERROR
#endif // _WIN32
#include "protos/imgui_test_harness.pb.h"
#ifdef _WIN32
#pragma pop_macro("DWORD")
#pragma pop_macro("ERROR")
#endif // _WIN32
#include "app/gui/automation/widget_id_registry.h"
#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
#include "imgui_test_engine/imgui_te_context.h"
#else
struct ImGuiTestContext;
#endif
namespace yaze {
namespace test {
// Service responsible for transforming widget registry data into
// DiscoverWidgetsResponse payloads.
class WidgetDiscoveryService {
public:
WidgetDiscoveryService() = default;
void CollectWidgets(ImGuiTestContext* ctx,
const DiscoverWidgetsRequest& request,
DiscoverWidgetsResponse* response) const;
private:
bool MatchesWindow(absl::string_view window_name,
absl::string_view filter) const;
bool MatchesPathPrefix(absl::string_view path,
absl::string_view prefix) const;
bool MatchesType(absl::string_view type,
WidgetType filter) const;
std::string ExtractWindowName(absl::string_view path) const;
std::string ExtractLabel(absl::string_view path) const;
std::string SuggestedAction(absl::string_view type,
absl::string_view label) const;
};
} // namespace test
} // namespace yaze
#endif // YAZE_APP_CORE_SERVICE_WIDGET_DISCOVERY_SERVICE_H_