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:
496
src/app/service/canvas_automation_service.cc
Normal file
496
src/app/service/canvas_automation_service.cc
Normal 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
|
||||
|
||||
147
src/app/service/canvas_automation_service.h
Normal file
147
src/app/service/canvas_automation_service.h
Normal 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_
|
||||
|
||||
1851
src/app/service/imgui_test_harness_service.cc
Normal file
1851
src/app/service/imgui_test_harness_service.cc
Normal file
File diff suppressed because it is too large
Load Diff
166
src/app/service/imgui_test_harness_service.h
Normal file
166
src/app/service/imgui_test_harness_service.h
Normal 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_
|
||||
264
src/app/service/screenshot_utils.cc
Normal file
264
src/app/service/screenshot_utils.cc
Normal 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, ®ion_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
|
||||
54
src/app/service/screenshot_utils.h
Normal file
54
src/app/service/screenshot_utils.h
Normal 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_
|
||||
183
src/app/service/unified_grpc_server.cc
Normal file
183
src/app/service/unified_grpc_server.cc
Normal 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
|
||||
149
src/app/service/unified_grpc_server.h
Normal file
149
src/app/service/unified_grpc_server.h
Normal 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
|
||||
259
src/app/service/widget_discovery_service.cc
Normal file
259
src/app/service/widget_discovery_service.cc
Normal 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
|
||||
57
src/app/service/widget_discovery_service.h
Normal file
57
src/app/service/widget_discovery_service.h
Normal 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_
|
||||
Reference in New Issue
Block a user