diff --git a/cmake/PanoPainterSources.cmake b/cmake/PanoPainterSources.cmake index 87905c0..bc3dd56 100644 --- a/cmake/PanoPainterSources.cmake +++ b/cmake/PanoPainterSources.cmake @@ -70,6 +70,8 @@ set(PP_PANOPAINTER_APP_SOURCES src/app_layout.cpp src/app_shaders.cpp src/app_vr.cpp + src/platform_legacy/legacy_platform_services.cpp + src/platform_legacy/legacy_platform_services.h src/version.cpp ) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 4f7ade7..474c1ea 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -450,7 +450,8 @@ Known local toolchain state: now uses injected `WindowsPlatformServices` from `src/platform_windows/windows_platform_services.*` in `pp_platform_windows`, while non-Windows platforms still reach retained platform bridges through - the debt-tracked legacy adapter in `app_events.cpp`. The iOS/Web + the debt-tracked adapter isolated in + `src/platform_legacy/legacy_platform_services.*`. The iOS/Web save-with-writer overload remains a separate app method until export handoff is isolated. - `pano_cli plan-cloud-upload` exposes `pp_app_core` cloud upload availability, diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index d2493c8..c60375d 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -35,7 +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`, and Windows live execution uses injected `WindowsPlatformServices`, but macOS cursor execution still reaches the retained fallback adapter 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 injected `pp_platform_*` services for every supported platform | | 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`, and Windows live execution uses injected `WindowsPlatformServices`, but Apple/Android clipboard execution still reaches retained fallback adapter branches 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 injected `pp_platform_*` services for every supported platform | -| DEBT-0017 | Open | Modernization | `App::clipboard_get_text`, `App::clipboard_set_text`, `App::show_cursor`, `App::hide_cursor`, `App::showKeyboard`, `App::hideKeyboard`, `App::display_file`, `App::share_file`, `App::pick_image`, `App::pick_file`, the non-writer `App::pick_file_save`, and `App::pick_dir` now call the SDK-free `pp::platform::PlatformServices` interface, and Windows injects `WindowsPlatformServices` from `src/platform_windows/windows_platform_services.*`; non-Windows live implementations still use a legacy fallback adapter in `app_events.cpp` that forwards to retained Apple/Android/Linux/Web bridge functions and retained no-op branches; the iOS/Web save-with-writer overload remains separate until export handoff is isolated | 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 non-Windows platform shell | +| DEBT-0017 | Open | Modernization | `App::clipboard_get_text`, `App::clipboard_set_text`, `App::show_cursor`, `App::hide_cursor`, `App::showKeyboard`, `App::hideKeyboard`, `App::display_file`, `App::share_file`, `App::pick_image`, `App::pick_file`, the non-writer `App::pick_file_save`, and `App::pick_dir` now call the SDK-free `pp::platform::PlatformServices` interface, and Windows injects `WindowsPlatformServices` from `src/platform_windows/windows_platform_services.*`; non-Windows live implementations still use `src/platform_legacy/legacy_platform_services.*`, a named fallback adapter that forwards to retained Apple/Android/Linux/Web bridge functions and retained no-op branches; the iOS/Web save-with-writer overload remains separate until export handoff is isolated | 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 `src/platform_legacy/legacy_platform_services.*` with injected `pp_platform_*` service implementations owned by each non-Windows platform shell | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 5e3952a..62e3ed7 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -474,8 +474,8 @@ display, file sharing, image/file/save-file pickers, and directory pickers. Windows installs an injected `WindowsPlatformServices` implementation from `src/platform_windows/windows_platform_services.*` in `pp_platform_windows`; other platforms still route through the debt-tracked legacy fallback adapter -in `app_events.cpp`, so behavior is preserved while their platform shell -implementations are extracted. The iOS/Web +now isolated in `src/platform_legacy/legacy_platform_services.*`, so behavior +is preserved while their platform shell implementations are extracted. The iOS/Web save-with-writer overload remains separate because it writes a temporary/exported file before handing control to the platform. `pano_cli plan-cloud-upload` exposes the app-core cloud upload decision used by @@ -995,7 +995,9 @@ Results: live Windows app now consumes this interface through an injected `WindowsPlatformServices` instance isolated in `src/platform_windows/windows_platform_services.*`; other platforms still - use the legacy fallback adapter. + use the legacy fallback adapter, now isolated in + `src/platform_legacy/legacy_platform_services.*` instead of being owned by + `app_events.cpp`. - `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 5117585..6fcf516 100644 --- a/src/app_events.cpp +++ b/src/app_events.cpp @@ -3,6 +3,7 @@ #include "app_core/document_platform_io.h" #include "app_core/document_sharing.h" #include "platform_api/platform_services.h" +#include "platform_legacy/legacy_platform_services.h" #include "renderer_gl/opengl_capabilities.h" namespace { @@ -12,14 +13,6 @@ namespace { return static_cast(pp::renderer::gl::rgba8_internal_format()); } -void invoke_picked_path_if_selected( - const std::string& path, - const std::function& callback) -{ - if (pp::app::plan_picked_path(path) == pp::app::PickedPathAction::invoke_callback) - callback(path); -} - [[nodiscard]] bool should_dispatch_keyboard_visibility(bool visible) noexcept { const auto action = pp::app::plan_virtual_keyboard(visible); @@ -38,204 +31,13 @@ void invoke_picked_path_if_selected( } -#ifdef __ANDROID__ -void displayKeyboard(bool pShow); -void android_pick_file(std::function callback); -void android_pick_file_save(std::function callback); -std::string android_get_clipboard(); -bool android_set_clipboard(const std::string& s); -#elif __APPLE__ -#elif __LINUX__ -#include -#elif __WEB__ -void webgl_pick_file(std::function callback); +#ifdef __WEB__ void webgl_pick_file_save(const std::string& path, const std::string& name, std::function callback); -void webgl_sync(); #endif namespace { -// DEBT-0017: fallback for platforms that do not inject PlatformServices yet. -class LegacyPlatformServices final : public pp::platform::PlatformServices { -public: - [[nodiscard]] std::string clipboard_text() override - { -#if __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 __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 __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 - } - - void pick_image(pp::platform::PickedPathCallback callback) override - { -#ifdef __IOS__ - dispatch_async(dispatch_get_main_queue(), ^{ - [App::I->ios_view pick_photo:callback]; - }); -#elif __OSX__ - dispatch_async(dispatch_get_main_queue(), ^{ - NSArray* fileTypes = [NSArray arrayWithObjects:@"png", @"PNG", @"jpg", @"JPG", @"jpeg", nil]; - std::string path = [App::I->osx_view pick_file:fileTypes]; - invoke_picked_path_if_selected(path, callback); - }); -#elif __ANDROID__ - android_pick_file(callback); -#elif __LINUX__ - if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false)) - invoke_picked_path_if_selected(p, callback); -#elif __WEB__ - webgl_pick_file(callback); -#else - (void)callback; -#endif - } - - void pick_file(std::vector file_types, pp::platform::PickedPathCallback callback) override - { -#ifdef __IOS__ - dispatch_async(dispatch_get_main_queue(), ^{ - NSMutableArray* fileTypes = [NSMutableArray arrayWithCapacity:file_types.size()]; - for (const auto& t : file_types) - [fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]]; - [App::I->ios_view pick_file:fileTypes then:callback]; - }); -#elif __OSX__ - dispatch_async(dispatch_get_main_queue(), ^{ - NSMutableArray* fileTypes = [NSMutableArray arrayWithCapacity:file_types.size()]; - for (const auto& t : file_types) - [fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]]; - std::string path = [App::I->osx_view pick_file:fileTypes]; - invoke_picked_path_if_selected(path, callback); - }); -#elif __ANDROID__ - android_pick_file(callback); -#elif __LINUX__ - if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false)) - invoke_picked_path_if_selected(p, callback); -#elif __WEB__ - webgl_pick_file(callback); -#else - (void)file_types; - (void)callback; -#endif - } - - void pick_save_file(std::vector file_types, pp::platform::PickedPathCallback callback) override - { -#if __OSX__ - dispatch_async(dispatch_get_main_queue(), ^{ - NSMutableArray* fileTypes = [NSMutableArray arrayWithCapacity:file_types.size()]; - for (const auto& t : file_types) - [fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]]; - std::string path = [App::I->osx_view pick_file_save:fileTypes]; - invoke_picked_path_if_selected(path, callback); - }); -#elif __ANDROID__ - android_pick_file_save(callback); -#else - (void)file_types; - (void)callback; -#endif - } - - void pick_directory(pp::platform::PickedPathCallback callback) override - { -#ifdef __IOS__ - (void)callback; -#elif __OSX__ - dispatch_async(dispatch_get_main_queue(), ^{ - std::string path = [App::I->osx_view pick_dir]; - invoke_picked_path_if_selected(path, callback); - }); -#elif __ANDROID__ - (void)callback; -#else - (void)callback; -#endif - } - - void display_file(std::string_view path) override - { - const std::string value(path); -#ifdef __IOS__ - dispatch_async(dispatch_get_main_queue(), ^{ - [App::I->ios_view display_file:value]; - }); -#elif __OSX__ - [[NSWorkspace sharedWorkspace] openFile:[NSString stringWithUTF8String:value.c_str()]]; -#else - (void)value; -#endif - } - - void share_file(std::string_view path) override - { - const std::string value(path); -#ifdef __IOS__ - dispatch_async(dispatch_get_main_queue(), ^{ - [App::I->ios_view share_file:[NSString stringWithUTF8String:value.c_str()]]; - }); -#elif __OSX__ - dispatch_async(dispatch_get_main_queue(), ^{ - [App::I->osx_view share_file:[NSString stringWithUTF8String:value.c_str()]]; - }); -#else - (void)value; -#endif - } -}; - -[[nodiscard]] pp::platform::PlatformServices& legacy_platform_services() -{ - static LegacyPlatformServices services; - return services; -} - [[nodiscard]] pp::platform::PlatformServices& active_platform_services() { if (App::I) @@ -243,7 +45,7 @@ public: if (auto* services = App::I->platform_services()) return *services; } - return legacy_platform_services(); + return pp::platform::legacy::platform_services(); } } diff --git a/src/platform_legacy/legacy_platform_services.cpp b/src/platform_legacy/legacy_platform_services.cpp new file mode 100644 index 0000000..424dbba --- /dev/null +++ b/src/platform_legacy/legacy_platform_services.cpp @@ -0,0 +1,219 @@ +#include "pch.h" +#include "platform_legacy/legacy_platform_services.h" + +#include "app.h" +#include "app_core/document_platform_io.h" + +#ifdef __ANDROID__ +void displayKeyboard(bool pShow); +void android_pick_file(std::function callback); +void android_pick_file_save(std::function callback); +std::string android_get_clipboard(); +bool android_set_clipboard(const std::string& s); +#elif __APPLE__ +#elif __LINUX__ +#include +#elif __WEB__ +void webgl_pick_file(std::function callback); +void webgl_pick_file_save( + const std::string& path, + const std::string& name, + std::function callback); +void webgl_sync(); +#endif + +namespace { + +void invoke_picked_path_if_selected( + const std::string& path, + const std::function& callback) +{ + if (pp::app::plan_picked_path(path) == pp::app::PickedPathAction::invoke_callback) + callback(path); +} + +// DEBT-0017: fallback for platforms that do not inject PlatformServices yet. +class LegacyPlatformServices final : public pp::platform::PlatformServices { +public: + [[nodiscard]] std::string clipboard_text() override + { +#if __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 __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 __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 + } + + void pick_image(pp::platform::PickedPathCallback callback) override + { +#ifdef __IOS__ + dispatch_async(dispatch_get_main_queue(), ^{ + [App::I->ios_view pick_photo:callback]; + }); +#elif __OSX__ + dispatch_async(dispatch_get_main_queue(), ^{ + NSArray* fileTypes = [NSArray arrayWithObjects:@"png", @"PNG", @"jpg", @"JPG", @"jpeg", nil]; + std::string path = [App::I->osx_view pick_file:fileTypes]; + invoke_picked_path_if_selected(path, callback); + }); +#elif __ANDROID__ + android_pick_file(callback); +#elif __LINUX__ + if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false)) + invoke_picked_path_if_selected(p, callback); +#elif __WEB__ + webgl_pick_file(callback); +#else + (void)callback; +#endif + } + + void pick_file(std::vector file_types, pp::platform::PickedPathCallback callback) override + { +#ifdef __IOS__ + dispatch_async(dispatch_get_main_queue(), ^{ + NSMutableArray* fileTypes = [NSMutableArray arrayWithCapacity:file_types.size()]; + for (const auto& t : file_types) + [fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]]; + [App::I->ios_view pick_file:fileTypes then:callback]; + }); +#elif __OSX__ + dispatch_async(dispatch_get_main_queue(), ^{ + NSMutableArray* fileTypes = [NSMutableArray arrayWithCapacity:file_types.size()]; + for (const auto& t : file_types) + [fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]]; + std::string path = [App::I->osx_view pick_file:fileTypes]; + invoke_picked_path_if_selected(path, callback); + }); +#elif __ANDROID__ + android_pick_file(callback); +#elif __LINUX__ + if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false)) + invoke_picked_path_if_selected(p, callback); +#elif __WEB__ + webgl_pick_file(callback); +#else + (void)file_types; + (void)callback; +#endif + } + + void pick_save_file(std::vector file_types, pp::platform::PickedPathCallback callback) override + { +#if __OSX__ + dispatch_async(dispatch_get_main_queue(), ^{ + NSMutableArray* fileTypes = [NSMutableArray arrayWithCapacity:file_types.size()]; + for (const auto& t : file_types) + [fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]]; + std::string path = [App::I->osx_view pick_file_save:fileTypes]; + invoke_picked_path_if_selected(path, callback); + }); +#elif __ANDROID__ + android_pick_file_save(callback); +#else + (void)file_types; + (void)callback; +#endif + } + + void pick_directory(pp::platform::PickedPathCallback callback) override + { +#ifdef __IOS__ + (void)callback; +#elif __OSX__ + dispatch_async(dispatch_get_main_queue(), ^{ + std::string path = [App::I->osx_view pick_dir]; + invoke_picked_path_if_selected(path, callback); + }); +#elif __ANDROID__ + (void)callback; +#else + (void)callback; +#endif + } + + void display_file(std::string_view path) override + { + const std::string value(path); +#ifdef __IOS__ + dispatch_async(dispatch_get_main_queue(), ^{ + [App::I->ios_view display_file:value]; + }); +#elif __OSX__ + [[NSWorkspace sharedWorkspace] openFile:[NSString stringWithUTF8String:value.c_str()]]; +#else + (void)value; +#endif + } + + void share_file(std::string_view path) override + { + const std::string value(path); +#ifdef __IOS__ + dispatch_async(dispatch_get_main_queue(), ^{ + [App::I->ios_view share_file:[NSString stringWithUTF8String:value.c_str()]]; + }); +#elif __OSX__ + dispatch_async(dispatch_get_main_queue(), ^{ + [App::I->osx_view share_file:[NSString stringWithUTF8String:value.c_str()]]; + }); +#else + (void)value; +#endif + } +}; + +} + +namespace pp::platform::legacy { + +PlatformServices& platform_services() +{ + static LegacyPlatformServices services; + return services; +} + +} diff --git a/src/platform_legacy/legacy_platform_services.h b/src/platform_legacy/legacy_platform_services.h new file mode 100644 index 0000000..67cdfc4 --- /dev/null +++ b/src/platform_legacy/legacy_platform_services.h @@ -0,0 +1,9 @@ +#pragma once + +#include "platform_api/platform_services.h" + +namespace pp::platform::legacy { + +[[nodiscard]] PlatformServices& platform_services(); + +}