From 4ed72ebc80489719d0bbf9e96b7494f60c7fba4f Mon Sep 17 00:00:00 2001 From: omigamedev Date: Wed, 3 Jun 2026 03:59:59 +0200 Subject: [PATCH] Introduce platform services interface --- CMakeLists.txt | 13 ++ docs/modernization/build-inventory.md | 9 +- docs/modernization/capability-map.md | 6 +- docs/modernization/debt.md | 3 +- docs/modernization/roadmap.md | 19 ++- src/app_events.cpp | 112 +++++++++++++----- src/platform_api/platform_services.cpp | 7 ++ src/platform_api/platform_services.h | 18 +++ tests/CMakeLists.txt | 10 ++ .../platform_api/platform_services_tests.cpp | 100 ++++++++++++++++ 10 files changed, 259 insertions(+), 38 deletions(-) create mode 100644 src/platform_api/platform_services.cpp create mode 100644 src/platform_api/platform_services.h create mode 100644 tests/platform_api/platform_services_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f14e19..88e4234 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -210,6 +210,18 @@ target_link_libraries(pp_ui_core pp_xml_tinyxml2 pp_project_warnings) +add_library(pp_platform_api STATIC + src/platform_api/platform_services.cpp + src/platform_api/platform_services.h) +target_include_directories(pp_platform_api + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/src") +target_link_libraries(pp_platform_api + PUBLIC + pp_project_options + PRIVATE + pp_project_warnings) + add_library(pp_app_core STATIC src/app_core/document_cloud.h src/app_core/document_export.cpp @@ -482,6 +494,7 @@ if(PP_BUILD_APP) pp_app_core pp_legacy_app pp_panopainter_ui + pp_platform_api pp_project_options PRIVATE pp_project_warnings) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 1a66ce6..790902f 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -10,7 +10,7 @@ Keep it updated as platform paths move to shared CMake targets. | Platform/Target | Current Entrypoint | Notes | | --- | --- | --- | -| Windows desktop | Root `CMakeLists.txt`, preset `windows-msvc-default`; target preset `windows-vs2026-x64` retained for VS 2026 | Raw `.sln/.vcxproj` files removed on 2026-05-31; local machine currently uses Visual Studio 17 2022; `PanoPainter` now links through `pp_platform_windows` and `panopainter_app`, with Windows/vendor link dependencies owned by the platform shell, runtime payload deployment in `cmake/PanoPainterRuntime.cmake`, tested app-level document-open routing plus open/close/save session decisions owned by `pp_app_core`, retained third-party source dependencies contained by `pp_legacy_vendor`, retained asset/file/serialization sources contained by `pp_legacy_assets_io`, retained paint/document/canvas sources contained by `pp_legacy_paint_document`, retained OpenGL runtime sources contained by `pp_legacy_renderer_gl` and folded into `pp_legacy_engine`, retained runtime shell sources contained by `pp_legacy_engine`, retained base UI controls contained by `pp_legacy_ui_core` and folded into `pp_legacy_app`, app orchestration/version metadata owned by `panopainter_app`, and app-specific modal/dialog/panel/canvas workflow nodes owned by `pp_panopainter_ui` | +| Windows desktop | Root `CMakeLists.txt`, preset `windows-msvc-default`; target preset `windows-vs2026-x64` retained for VS 2026 | Raw `.sln/.vcxproj` files removed on 2026-05-31; local machine currently uses Visual Studio 17 2022; `PanoPainter` now links through `pp_platform_windows` and `panopainter_app`, with Windows/vendor link dependencies owned by the platform shell, runtime payload deployment in `cmake/PanoPainterRuntime.cmake`, tested app-level document-open routing plus open/close/save session decisions owned by `pp_app_core`, SDK-free clipboard/cursor/virtual-keyboard service contracts owned by `pp_platform_api`, retained third-party source dependencies contained by `pp_legacy_vendor`, retained asset/file/serialization sources contained by `pp_legacy_assets_io`, retained paint/document/canvas sources contained by `pp_legacy_paint_document`, retained OpenGL runtime sources contained by `pp_legacy_renderer_gl` and folded into `pp_legacy_engine`, retained runtime shell sources contained by `pp_legacy_engine`, retained base UI controls contained by `pp_legacy_ui_core` and folded into `pp_legacy_app`, app orchestration/version metadata owned by `panopainter_app`, and app-specific modal/dialog/panel/canvas workflow nodes owned by `pp_panopainter_ui` | | Windows AppX | `PanoPainterPackage/Package.appxmanifest`, `.wapproj` referenced by solution | Distribution packaging | | macOS | `PanoPainter-OSX/` project files and `Info.plist` | Uses `NSOpenGLView` today | | iOS | `PanoPainter/Info.plist`, related Apple sources | Uses OpenGL ES today | @@ -444,6 +444,10 @@ Known local toolchain state: `pp_app_core` clipboard text planning as JSON, including empty text writes; live clipboard get/set requests consume the same contracts before retained platform clipboard bridges. +- `pp_platform_api` exposes the SDK-free `PlatformServices` interface for + clipboard text, cursor visibility, and virtual-keyboard visibility; live + app execution now reaches retained platform bridges through the + debt-tracked legacy adapter in `app_events.cpp`. - `pano_cli plan-cloud-upload` exposes `pp_app_core` cloud upload availability, new-document warning, publish prompt, and save-before-upload planning as JSON; the live cloud upload command consumes the same start contract before @@ -480,6 +484,9 @@ Known local toolchain state: show/hide planning before platform keyboard callbacks, plus cursor visibility planning before platform cursor callbacks, plus clipboard read/write planning before platform clipboard callbacks. +- `pp_platform_api_tests` covers service dispatch for clipboard read/write, + empty clipboard writes, cursor visibility, and virtual-keyboard visibility + without platform SDK headers or a window. - `pp_app_core_document_cloud_tests` covers cloud upload no-canvas, new-document warning, clean publish prompt, and dirty save-before-upload decisions, plus cloud browse no-canvas/show-browser and selected-download diff --git a/docs/modernization/capability-map.md b/docs/modernization/capability-map.md index 91cd5cc..36be5a7 100644 --- a/docs/modernization/capability-map.md +++ b/docs/modernization/capability-map.md @@ -65,10 +65,10 @@ and validation command. | Capability | Current Area | Target Owner | Required Tests | | --- | --- | --- | --- | -| Mouse/keyboard/touch/gestures/cursor | `App`, platform entrypoints | `pp_app_core`, `pp_platform_*`, app | Cursor visibility decision tests, synthetic event playback | +| Mouse/keyboard/touch/gestures/cursor | `App`, platform entrypoints | `pp_app_core`, `pp_platform_api`, `pp_platform_*`, app | Cursor visibility decision tests, platform service dispatch tests, synthetic event playback | | Wacom pressure | `WacomTablet` | `pp_platform_windows` | Adapter smoke with fallback | -| Clipboard/file picker/share/display | `App` platform methods | `pp_app_core`, `pp_platform_*` | Clipboard read/write, share saved-path, picked-path, and display-file decision tests, platform smoke or mocked service | -| Virtual keyboard | `App`, platform entrypoints | `pp_app_core`, `pp_platform_*` | Keyboard visibility decision tests, platform smoke | +| Clipboard/file picker/share/display | `App` platform methods | `pp_app_core`, `pp_platform_api`, `pp_platform_*` | Clipboard read/write, share saved-path, picked-path, and display-file decision tests, platform service dispatch tests, platform smoke or mocked service | +| Virtual keyboard | `App`, platform entrypoints | `pp_app_core`, `pp_platform_api`, `pp_platform_*` | Keyboard visibility decision tests, platform service dispatch tests, platform smoke | | OpenVR desktop | `HMD`, `Vive`, `app_vr` | `pp_platform_vr`, app | Compile gate and mocked pose tests | | Quest/OVR | Android Quest files | `pp_platform_android_quest` | Compile/package gate | | Focus/Wave | Android Focus files | `pp_platform_android_wave` | Compile/package gate | diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 574bb00..b71b433 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -1,7 +1,7 @@ # Modernization Debt Log Status: live -Last updated: 2026-06-02 +Last updated: 2026-06-03 Every shortcut, temporary adapter, retained vendored dependency, skipped platform gate, compatibility shim, or incomplete automation path must be @@ -35,6 +35,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0014 | Open | Modernization | `windows-clangcl-asan` now configures as a headless Ninja/clang-cl preset and uses the release MSVC runtime required by ASan, but local builds still fail because installed clang-cl 18.1.8 is paired with VS 2026-preview STL headers that require Clang 20 or newer | Sanitizer validation should be local and repeatable, but this machine's compiler/header pairing is incompatible | `cmake --fresh --preset windows-clangcl-asan`; `cmake --build --preset windows-clangcl-asan --target pp_foundation` | Install/use Clang 20+ with the VS 2026 STL, or point the preset at a compatible VS 2022 toolchain, then make `platform-build.ps1 -Presets windows-clangcl-asan` pass for the headless matrix | | DEBT-0015 | Open | Modernization | Cursor visibility requests now consume pure `pp_app_core` planning through `pano_cli plan-cursor-visibility`, but live cursor execution still reaches retained Win32/macOS platform bridges from `App::show_cursor` and `App::hide_cursor` | Keep canvas cursor behavior stable while platform shells are extracted incrementally | `pp_app_core_document_platform_io_tests`; `pano_cli plan-cursor-visibility --visible`; `ctest --preset desktop-fast --build-config Debug` | Cursor visibility execution is owned by `pp_platform_*` services and live app code depends on an injected platform interface instead of direct singleton/platform calls | | DEBT-0016 | Open | Modernization | Clipboard get/set requests now consume pure `pp_app_core` planning through `pano_cli plan-clipboard-read` and `pano_cli plan-clipboard-write`, but live clipboard execution still reaches retained Win32/Apple/Android platform bridges from `App::clipboard_get_text` and `App::clipboard_set_text` | Keep picker/color text clipboard behavior stable while platform shells are extracted incrementally | `pp_app_core_document_platform_io_tests`; `pano_cli plan-clipboard-write --text #ff00aa`; `ctest --preset desktop-fast --build-config Debug` | Clipboard execution is owned by `pp_platform_*` services and live app code depends on an injected platform interface instead of direct singleton/platform calls | +| DEBT-0017 | Open | Modernization | `App::clipboard_get_text`, `App::clipboard_set_text`, `App::show_cursor`, `App::hide_cursor`, `App::showKeyboard`, and `App::hideKeyboard` now call the SDK-free `pp::platform::PlatformServices` interface, but the live implementation is still a legacy adapter in `app_events.cpp` that forwards to retained Win32/Apple/Android bridge functions | Preserve behavior while moving platform execution behind a testable service boundary before platform shell implementations are injected | `pp_platform_api_tests`; `pp_app_core_document_platform_io_tests`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Replace the `app_events.cpp` legacy adapter with injected `pp_platform_*` service implementations owned by each platform shell | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index ff058ff..cf3ea73 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -1,7 +1,7 @@ # PanoPainter Modernization Roadmap Status: live -Last updated: 2026-06-02 +Last updated: 2026-06-03 This is the living roadmap for modernizing PanoPainter into independently testable C++23 components while retaining all existing functionality. Keep this @@ -87,6 +87,8 @@ Intended responsibilities: - `pp_ui_core`: `Node`, layout, generic controls, text/image primitives. - `pp_panopainter_ui`: panels, dialogs, `NodeCanvas`, and app-specific workflows. +- `pp_platform_api`: SDK-free service interfaces for clipboard, cursor, + virtual keyboard, file pickers, sharing, and future platform automation. - `pp_platform_*`: Windows, macOS/iOS, Android, Linux, and WebGL shells. - `panopainter_app`: composition root only. @@ -466,6 +468,11 @@ cursor bridges continue. `pano_cli plan-clipboard-read` and `pano_cli plan-clipboard-write` expose the app-core clipboard text decisions used by live clipboard get/set requests before retained platform clipboard bridges continue. +`pp_platform_api` now owns a headless `PlatformServices` interface for +clipboard text, cursor visibility, and virtual-keyboard visibility. Live app +clipboard/cursor/keyboard execution routes through a debt-tracked legacy +adapter in `app_events.cpp`, so behavior is preserved while later platform +shell implementations can replace direct bridge calls. `pano_cli plan-cloud-upload` exposes the app-core cloud upload decision used by the live cloud upload command for missing-canvas, new-document warning, publish prompt, and dirty-document save-before-upload states before legacy UI, canvas, @@ -494,8 +501,9 @@ Implementation tasks: 7. `pp_paint_renderer` 8. `pp_ui_core` 9. `pp_panopainter_ui` - 10. `pp_platform_*` - 11. `panopainter_app` + 10. `pp_platform_api` + 11. `pp_platform_*` + 12. `panopainter_app` - Remove renderer/platform dependencies from pure headers first, especially: - `Brush` - document/layer model @@ -975,6 +983,11 @@ Results: `pano_cli_plan_clipboard_write_smoke`, and `pano_cli_plan_clipboard_write_empty_smoke` passed and expose app-core clipboard decisions as JSON, including empty write text. +- `pp_platform_api_tests` passed, covering the SDK-free `PlatformServices` + interface for clipboard read/write, empty clipboard writes, cursor + visibility dispatch, and virtual-keyboard visibility dispatch. The live app + now consumes this interface through the legacy platform adapter for + clipboard/cursor/keyboard execution. - `panopainter_validate_shaders` passed, validating 25 shader programs and 7 shader includes for stage markers and include graph integrity. - `pp_renderer_gl_capabilities_tests` passed on default MSVC, vcpkg-headless, diff --git a/src/app_events.cpp b/src/app_events.cpp index 1f7c42b..6e026ff 100644 --- a/src/app_events.cpp +++ b/src/app_events.cpp @@ -2,6 +2,7 @@ #include "app.h" #include "app_core/document_platform_io.h" #include "app_core/document_sharing.h" +#include "platform_api/platform_services.h" #include "renderer_gl/opengl_capabilities.h" namespace { @@ -60,21 +61,84 @@ void webgl_pick_file_save(const std::string& path, void webgl_sync(); #endif +namespace { + +class LegacyPlatformServices final : public pp::platform::PlatformServices { +public: + [[nodiscard]] std::string clipboard_text() override + { +#if _WIN32 + return win32_clipboard_get_text(); +#elif __IOS__ + return [App::I->ios_view clipboard_get_string]; +#elif __OSX__ + return [App::I->osx_view clipboard_get_string]; +#elif __ANDROID__ + return android_get_clipboard(); +#else + return {}; +#endif + } + + [[nodiscard]] bool set_clipboard_text(std::string_view text) override + { + const std::string value(text); +#if _WIN32 + return win32_clipboard_set_text(value); +#elif __IOS__ + return [App::I->ios_view clipboard_set_string:value]; +#elif __OSX__ + return [App::I->osx_view clipboard_set_string:value]; +#elif __ANDROID__ + return android_set_clipboard(value); +#else + return false; +#endif + } + + void set_cursor_visible(bool visible) override + { +#ifdef _WIN32 + win32_show_cursor(visible); +#elif __OSX__ + [App::I->osx_view show_cursor:visible]; +#else + (void)visible; +#endif + } + + void set_virtual_keyboard_visible(bool visible) override + { +#ifdef __IOS__ + dispatch_async(dispatch_get_main_queue(), ^{ + if (visible) + [App::I->ios_view show_keyboard]; + else + [App::I->ios_view hide_keyboard]; + }); +#elif __ANDROID__ + displayKeyboard(visible); +#else + (void)visible; +#endif + } +}; + +[[nodiscard]] pp::platform::PlatformServices& legacy_platform_services() +{ + static LegacyPlatformServices services; + return services; +} + +} + std::string App::clipboard_get_text() { if (pp::app::plan_clipboard_read() != pp::app::ClipboardReadAction::read_text) return {}; -#if _WIN32 - return win32_clipboard_get_text(); -#elif __IOS__ - return [ios_view clipboard_get_string]; -#elif __OSX__ - return [osx_view clipboard_get_string]; -#elif __ANDROID__ - return android_get_clipboard(); -#endif + return legacy_platform_services().clipboard_text(); } bool App::clipboard_set_text(const std::string& s) @@ -82,15 +146,7 @@ bool App::clipboard_set_text(const std::string& s) if (pp::app::plan_clipboard_write(s) != pp::app::ClipboardWriteAction::write_text) return false; -#if _WIN32 - return win32_clipboard_set_text(s); -#elif __IOS__ - return [ios_view clipboard_set_string:s]; -#elif __OSX__ - return [osx_view clipboard_set_string:s]; -#elif __ANDROID__ - return android_set_clipboard(s); -#endif + return legacy_platform_services().set_clipboard_text(s); } void App::stacktrace() @@ -138,9 +194,9 @@ void App::show_cursor() return; #ifdef _WIN32 - win32_show_cursor(true); + legacy_platform_services().set_cursor_visible(true); #elif __OSX__ - [osx_view show_cursor:true]; + legacy_platform_services().set_cursor_visible(true); #endif } @@ -150,9 +206,9 @@ void App::hide_cursor() return; #ifdef _WIN32 - win32_show_cursor(false); + legacy_platform_services().set_cursor_visible(false); #elif __OSX__ - [osx_view show_cursor:false]; + legacy_platform_services().set_cursor_visible(false); #endif } @@ -163,11 +219,9 @@ void App::showKeyboard() if (!should_dispatch_keyboard_visibility(true)) return; #ifdef __IOS__ - dispatch_async(dispatch_get_main_queue(), ^{ - [ios_view show_keyboard]; - }); + legacy_platform_services().set_virtual_keyboard_visible(true); #elif __ANDROID__ - displayKeyboard(true); + legacy_platform_services().set_virtual_keyboard_visible(true); #endif } @@ -178,11 +232,9 @@ void App::hideKeyboard() if (!should_dispatch_keyboard_visibility(false)) return; #ifdef __IOS__ - dispatch_async(dispatch_get_main_queue(), ^{ - [ios_view hide_keyboard]; - }); + legacy_platform_services().set_virtual_keyboard_visible(false); #elif __ANDROID__ - displayKeyboard(false); + legacy_platform_services().set_virtual_keyboard_visible(false); #endif } diff --git a/src/platform_api/platform_services.cpp b/src/platform_api/platform_services.cpp new file mode 100644 index 0000000..dbd074c --- /dev/null +++ b/src/platform_api/platform_services.cpp @@ -0,0 +1,7 @@ +#include "platform_api/platform_services.h" + +namespace pp::platform { +namespace { +static_assert(sizeof(PlatformServices*) == sizeof(void*)); +} +} diff --git a/src/platform_api/platform_services.h b/src/platform_api/platform_services.h new file mode 100644 index 0000000..2ff1346 --- /dev/null +++ b/src/platform_api/platform_services.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +namespace pp::platform { + +class PlatformServices { +public: + virtual ~PlatformServices() = default; + + [[nodiscard]] virtual std::string clipboard_text() = 0; + [[nodiscard]] virtual bool set_clipboard_text(std::string_view text) = 0; + virtual void set_cursor_visible(bool visible) = 0; + virtual void set_virtual_keyboard_visible(bool visible) = 0; +}; + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b6fac41..4915f50 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -228,6 +228,16 @@ add_test(NAME pp_paint_renderer_compositor_tests COMMAND pp_paint_renderer_compo set_tests_properties(pp_paint_renderer_compositor_tests PROPERTIES LABELS "renderer;paint;desktop-fast") +add_executable(pp_platform_api_tests + platform_api/platform_services_tests.cpp) +target_link_libraries(pp_platform_api_tests PRIVATE + pp_platform_api + pp_test_harness) + +add_test(NAME pp_platform_api_tests COMMAND pp_platform_api_tests) +set_tests_properties(pp_platform_api_tests PROPERTIES + LABELS "os-api;desktop-fast") + add_executable(pp_ui_core_color_tests ui_core/color_tests.cpp) target_link_libraries(pp_ui_core_color_tests PRIVATE diff --git a/tests/platform_api/platform_services_tests.cpp b/tests/platform_api/platform_services_tests.cpp new file mode 100644 index 0000000..00b2352 --- /dev/null +++ b/tests/platform_api/platform_services_tests.cpp @@ -0,0 +1,100 @@ +#include "platform_api/platform_services.h" +#include "test_harness.h" + +#include +#include +#include + +namespace { + +class FakePlatformServices final : public pp::platform::PlatformServices { +public: + explicit FakePlatformServices(std::string clipboard_value) + : clipboard_value_(std::move(clipboard_value)) + { + } + + [[nodiscard]] std::string clipboard_text() override + { + ++clipboard_reads; + return clipboard_value_; + } + + [[nodiscard]] bool set_clipboard_text(std::string_view text) override + { + ++clipboard_writes; + clipboard_value_.assign(text); + return true; + } + + void set_cursor_visible(bool visible) override + { + ++cursor_updates; + cursor_visible = visible; + } + + void set_virtual_keyboard_visible(bool visible) override + { + ++keyboard_updates; + keyboard_visible = visible; + } + + int clipboard_reads = 0; + int clipboard_writes = 0; + int cursor_updates = 0; + int keyboard_updates = 0; + bool cursor_visible = false; + bool keyboard_visible = false; + +private: + std::string clipboard_value_; +}; + +void platform_services_dispatch_clipboard_reads_and_writes(pp::tests::Harness& harness) +{ + FakePlatformServices fake("#112233"); + pp::platform::PlatformServices& services = fake; + + PP_EXPECT(harness, services.clipboard_text() == "#112233"); + PP_EXPECT(harness, services.set_clipboard_text("#ff00aa")); + PP_EXPECT(harness, services.clipboard_text() == "#ff00aa"); + PP_EXPECT(harness, fake.clipboard_reads == 2); + PP_EXPECT(harness, fake.clipboard_writes == 1); +} + +void platform_services_preserve_empty_clipboard_writes(pp::tests::Harness& harness) +{ + FakePlatformServices fake("initial"); + pp::platform::PlatformServices& services = fake; + + PP_EXPECT(harness, services.set_clipboard_text("")); + PP_EXPECT(harness, services.clipboard_text().empty()); + PP_EXPECT(harness, fake.clipboard_writes == 1); +} + +void platform_services_dispatch_visibility_updates(pp::tests::Harness& harness) +{ + FakePlatformServices fake("unused"); + pp::platform::PlatformServices& services = fake; + + services.set_cursor_visible(true); + services.set_virtual_keyboard_visible(true); + services.set_cursor_visible(false); + services.set_virtual_keyboard_visible(false); + + PP_EXPECT(harness, fake.cursor_updates == 2); + PP_EXPECT(harness, fake.keyboard_updates == 2); + PP_EXPECT(harness, !fake.cursor_visible); + PP_EXPECT(harness, !fake.keyboard_visible); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run("platform services dispatch clipboard reads and writes", platform_services_dispatch_clipboard_reads_and_writes); + harness.run("platform services preserve empty clipboard writes", platform_services_preserve_empty_clipboard_writes); + harness.run("platform services dispatch visibility updates", platform_services_dispatch_visibility_updates); + return harness.finish(); +}