From c348f7f91f9ae8a5e1bbe8f63b914424c6cf2e3f Mon Sep 17 00:00:00 2001 From: scawful Date: Thu, 2 Oct 2025 23:16:15 -0400 Subject: [PATCH] guard JSON serialization for widget state capture for cross platform --- src/app/app.cmake | 7 +- src/app/core/core.cmake | 1 + src/app/core/widget_state_capture.cc | 195 +++++++++++++++++++++++---- 3 files changed, 177 insertions(+), 26 deletions(-) diff --git a/src/app/app.cmake b/src/app/app.cmake index 114e0571..fbb9e9dc 100644 --- a/src/app/app.cmake +++ b/src/app/app.cmake @@ -246,14 +246,15 @@ target_sources(yaze PRIVATE ${CMAKE_SOURCE_DIR}/src/app/core/widget_state_capture.cc ${CMAKE_SOURCE_DIR}/src/app/core/widget_state_capture.h) -target_include_directories(yaze PRIVATE - ${CMAKE_SOURCE_DIR}/third_party/json/include) - # ============================================================================ # Optional gRPC Support for ImGuiTestHarness # ============================================================================ if(YAZE_WITH_GRPC) message(STATUS "Adding gRPC ImGuiTestHarness to yaze target") + + target_include_directories(yaze PRIVATE + ${CMAKE_SOURCE_DIR}/third_party/json/include) + target_compile_definitions(yaze PRIVATE YAZE_WITH_JSON) # Generate C++ code from .proto using the helper function from cmake/grpc.cmake target_add_protobuf(yaze diff --git a/src/app/core/core.cmake b/src/app/core/core.cmake index e85095b6..06873872 100644 --- a/src/app/core/core.cmake +++ b/src/app/core/core.cmake @@ -5,6 +5,7 @@ set( app/core/project.cc app/core/window.cc app/core/asar_wrapper.cc + app/core/widget_state_capture.cc ) if (WIN32 OR MINGW OR (UNIX AND NOT APPLE)) diff --git a/src/app/core/widget_state_capture.cc b/src/app/core/widget_state_capture.cc index ef5d4a56..f866f19d 100644 --- a/src/app/core/widget_state_capture.cc +++ b/src/app/core/widget_state_capture.cc @@ -7,11 +7,85 @@ #else #include "imgui/imgui.h" #endif +#include + +#if defined(YAZE_WITH_JSON) #include "nlohmann/json.hpp" +#endif namespace yaze { namespace core { +#if !defined(YAZE_WITH_JSON) +namespace { + +std::string EscapeJsonString(const std::string& value) { + std::string escaped; + escaped.reserve(value.size() + 2); + escaped.push_back('"'); + + for (unsigned char c : value) { + switch (c) { + case '"': + escaped.append("\\\""); + break; + case '\\': + escaped.append("\\\\"); + break; + case '\b': + escaped.append("\\b"); + break; + case '\f': + escaped.append("\\f"); + break; + case '\n': + escaped.append("\\n"); + break; + case '\r': + escaped.append("\\r"); + break; + case '\t': + escaped.append("\\t"); + break; + default: + if (c <= 0x1F) { + escaped.append(absl::StrFormat("\\\\u%04X", static_cast(c))); + } else { + escaped.push_back(static_cast(c)); + } + break; + } + } + + escaped.push_back('"'); + return escaped; +} + +const char* BoolToJson(bool value) { return value ? "true" : "false"; } + +std::string FormatFloat(float value) { + // Match typical JSON formatting without trailing zeros when possible. + return absl::StrFormat("%.4f", value); +} + +std::string FormatFloatCompact(float value) { + std::string formatted = FormatFloat(value); + + // Trim trailing zeros while keeping at least one decimal place. + if (formatted.find('.') != std::string::npos) { + while (!formatted.empty() && formatted.back() == '0') { + formatted.pop_back(); + } + if (!formatted.empty() && formatted.back() == '.') { + formatted.push_back('0'); + } + } + return formatted; +} + +} // namespace +#endif // !defined(YAZE_WITH_JSON) + std::string CaptureWidgetState() { WidgetState state; @@ -81,48 +155,123 @@ std::string CaptureWidgetState() { // When UI test engine / ImGui internals aren't available, provide a minimal // payload so downstream systems still receive structured JSON. This keeps // builds that exclude the UI test engine (e.g., Windows release) working. - return R"({"warning": "Widget state capture unavailable (UI test engine disabled)"})"; + return "{\"warning\": \"Widget state capture unavailable (UI test engine disabled)\"}"; #endif return SerializeWidgetStateToJson(state); } std::string SerializeWidgetStateToJson(const WidgetState& state) { +#if defined(YAZE_WITH_JSON) nlohmann::json j; - - // Basic state + j["frame_count"] = state.frame_count; j["frame_rate"] = state.frame_rate; - - // Window state j["focused_window"] = state.focused_window; j["focused_widget"] = state.focused_widget; j["hovered_widget"] = state.hovered_widget; j["visible_windows"] = state.visible_windows; j["open_popups"] = state.open_popups; - - // Navigation state j["navigation"] = { - {"nav_id", absl::StrFormat("0x%08X", state.nav_id)}, - {"nav_active", state.nav_active} - }; - - // Input state + {"nav_id", absl::StrFormat("0x%08X", state.nav_id)}, + {"nav_active", state.nav_active}}; + nlohmann::json mouse_buttons; - for (int i = 0; i < 5; i++) { + for (int i = 0; i < 5; ++i) { mouse_buttons.push_back(state.mouse_down[i]); } + j["input"] = { - {"mouse_buttons", mouse_buttons}, - {"mouse_pos", {state.mouse_pos_x, state.mouse_pos_y}}, - {"modifiers", { - {"ctrl", state.ctrl_pressed}, - {"shift", state.shift_pressed}, - {"alt", state.alt_pressed} - }} - }; - - return j.dump(2); // Pretty print with 2-space indent + {"mouse_buttons", mouse_buttons}, + {"mouse_pos", {state.mouse_pos_x, state.mouse_pos_y}}, + {"modifiers", + {{"ctrl", state.ctrl_pressed}, + {"shift", state.shift_pressed}, + {"alt", state.alt_pressed}}}}; + + return j.dump(2); +#else + std::string json; + json.reserve(512); + + json.append("{\n"); + json.append(" \"frame_count\": "); + json.append(std::to_string(state.frame_count)); + json.append(",\n"); + + json.append(" \"frame_rate\": "); + json.append(FormatFloatCompact(state.frame_rate)); + json.append(",\n"); + + json.append(" \"focused_window\": "); + json.append(EscapeJsonString(state.focused_window)); + json.append(",\n"); + + json.append(" \"focused_widget\": "); + json.append(EscapeJsonString(state.focused_widget)); + json.append(",\n"); + + json.append(" \"hovered_widget\": "); + json.append(EscapeJsonString(state.hovered_widget)); + json.append(",\n"); + + json.append(" \"visible_windows\": ["); + for (size_t i = 0; i < state.visible_windows.size(); ++i) { + if (i > 0) { + json.append(", "); + } + json.append(EscapeJsonString(state.visible_windows[i])); + } + json.append("],\n"); + + json.append(" \"open_popups\": ["); + for (size_t i = 0; i < state.open_popups.size(); ++i) { + if (i > 0) { + json.append(", "); + } + json.append(EscapeJsonString(state.open_popups[i])); + } + json.append("],\n"); + + json.append(" \"navigation\": {\n"); + json.append(" \"nav_id\": "); + json.append(EscapeJsonString(absl::StrFormat("0x%08X", state.nav_id))); + json.append(",\n"); + json.append(" \"nav_active\": "); + json.append(BoolToJson(state.nav_active)); + json.append("\n },\n"); + + json.append(" \"input\": {\n"); + json.append(" \"mouse_buttons\": ["); + for (int i = 0; i < 5; ++i) { + if (i > 0) { + json.append(", "); + } + json.append(BoolToJson(state.mouse_down[i])); + } + json.append("],\n"); + + json.append(" \"mouse_pos\": ["); + json.append(FormatFloatCompact(state.mouse_pos_x)); + json.append(", "); + json.append(FormatFloatCompact(state.mouse_pos_y)); + json.append("],\n"); + + json.append(" \"modifiers\": {\n"); + json.append(" \"ctrl\": "); + json.append(BoolToJson(state.ctrl_pressed)); + json.append(",\n"); + json.append(" \"shift\": "); + json.append(BoolToJson(state.shift_pressed)); + json.append(",\n"); + json.append(" \"alt\": "); + json.append(BoolToJson(state.alt_pressed)); + json.append("\n }\n"); + json.append(" }\n"); + json.append("}\n"); + + return json; +#endif // defined(YAZE_WITH_JSON) } } // namespace core