Introduce platform services interface

This commit is contained in:
2026-06-03 03:59:59 +02:00
parent 6960bd3410
commit 4ed72ebc80
10 changed files with 259 additions and 38 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
#include "platform_api/platform_services.h"
namespace pp::platform {
namespace {
static_assert(sizeof(PlatformServices*) == sizeof(void*));
}
}

View File

@@ -0,0 +1,18 @@
#pragma once
#include <string>
#include <string_view>
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;
};
}

View File

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

View File

@@ -0,0 +1,100 @@
#include "platform_api/platform_services.h"
#include "test_harness.h"
#include <string>
#include <string_view>
#include <utility>
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();
}