From 46fb8efec464042a509a31a99fb0d9b0fe705c1d Mon Sep 17 00:00:00 2001 From: omigamedev Date: Fri, 12 Jun 2026 20:18:20 +0200 Subject: [PATCH] Integrate dialog export and Apple service teams --- CMakeLists.txt | 56 ++++- docs/modernization/debt.md | 38 +++- docs/modernization/tasks.md | 26 ++- src/app_core/app_dialog.h | 36 +++ src/app_core/document_export.h | 2 +- src/app_dialogs.cpp | 9 +- src/legacy_app_dialog_services.cpp | 150 +++++++++++-- src/legacy_app_dialog_services.h | 17 +- src/legacy_document_export_services.cpp | 68 +++++- src/legacy_ui_overlay_services.cpp | 44 ++++ src/legacy_ui_overlay_services.h | 16 ++ src/paint_renderer/compositor.cpp | 212 ++++++++++++++++++ src/paint_renderer/compositor.h | 20 ++ .../apple_platform_services.cpp | 128 +++++++++++ src/platform_apple/apple_platform_services.h | 44 ++++ .../legacy_platform_services.cpp | 135 +++++++---- tests/CMakeLists.txt | 2 +- tests/app_core/app_dialog_tests.cpp | 54 +++++ tests/app_core/document_export_tests.cpp | 17 +- tests/paint_renderer/compositor_tests.cpp | 145 ++++++++++++ .../platform_api/platform_services_tests.cpp | 174 ++++++++++++++ 21 files changed, 1271 insertions(+), 122 deletions(-) create mode 100644 src/platform_apple/apple_platform_services.cpp create mode 100644 src/platform_apple/apple_platform_services.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e8e420..460537d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -311,8 +311,24 @@ endif() if(PP_BUILD_APP) if(WIN32) + set(PP_LEGACY_FMT_SOURCES + libs/fmt/src/format.cc + libs/fmt/src/posix.cc) + set(PP_LEGACY_VENDOR_SOURCES ${PP_VENDOR_SOURCES}) + list(REMOVE_ITEM PP_LEGACY_VENDOR_SOURCES ${PP_LEGACY_FMT_SOURCES}) + set(PP_LEGACY_VENDOR_DEFINITIONS + ENUM_BITFIELDS_NOT_SUPPORTED + UNICODE + _UNICODE + _CRT_SECURE_NO_WARNINGS + _SCL_SECURE_NO_WARNINGS + _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING + _SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING + _CONSOLE + WITH_CURL=1) + add_library(pp_legacy_vendor OBJECT - ${PP_VENDOR_SOURCES}) + ${PP_LEGACY_VENDOR_SOURCES}) target_link_libraries(pp_legacy_vendor PUBLIC pp_project_options @@ -322,24 +338,39 @@ if(PP_BUILD_APP) PUBLIC ${PP_LEGACY_INCLUDE_DIRS}) file(REMOVE_RECURSE "${CMAKE_CURRENT_BINARY_DIR}/compat/fmt-vs2026") + + add_library(pp_legacy_fmt OBJECT + ${PP_LEGACY_FMT_SOURCES}) + target_link_libraries(pp_legacy_fmt + PUBLIC + pp_project_options + PRIVATE + pp_project_warnings) + target_include_directories(pp_legacy_fmt + PUBLIC + ${PP_LEGACY_INCLUDE_DIRS}) if(MSVC_VERSION GREATER_EQUAL 1945) - target_compile_options(pp_legacy_vendor + set(PP_FMT_VS2026_COMPAT_HEADER "${CMAKE_CURRENT_BINARY_DIR}/compat/fmt-vs2026-secure-scl.h") + file(WRITE "${PP_FMT_VS2026_COMPAT_HEADER}" + "#pragma once\n" + "#include \n" + "#ifdef _SECURE_SCL\n" + "#undef _SECURE_SCL\n" + "#endif\n") + target_compile_options(pp_legacy_fmt PUBLIC - /U_SECURE_SCL) + /FI"${PP_FMT_VS2026_COMPAT_HEADER}") endif() target_compile_definitions(pp_legacy_vendor PUBLIC - ENUM_BITFIELDS_NOT_SUPPORTED - UNICODE - _UNICODE - _CRT_SECURE_NO_WARNINGS - _SCL_SECURE_NO_WARNINGS - _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING - _SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING - _CONSOLE - WITH_CURL=1) + ${PP_LEGACY_VENDOR_DEFINITIONS}) + target_compile_definitions(pp_legacy_fmt + PUBLIC + ${PP_LEGACY_VENDOR_DEFINITIONS}) set_target_properties(pp_legacy_vendor PROPERTIES VS_GLOBAL_CharacterSet "Unicode") + set_target_properties(pp_legacy_fmt PROPERTIES + VS_GLOBAL_CharacterSet "Unicode") add_library(pp_legacy_renderer_gl OBJECT ${PP_LEGACY_RENDERER_GL_SOURCES}) @@ -436,6 +467,7 @@ if(PP_BUILD_APP) $ $ $ + $ $) target_link_libraries(pp_legacy_engine diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 1053881..18a09dc 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -18,11 +18,35 @@ agent or engineer to remove them without reconstructing context from chat. ## Recent Reductions +- 2026-06-12: DEBT-0060 was narrowed. Retained Android standard/Quest/Focus + package CMake files no longer generate or prepend a patched `nanort.h` + overlay, and the compatibility helper was removed; retained Android standard + validation now gets past `nanort` and fails later in `legacy_gl_runtime_dispatch.h` + because Android/GLES does not expose desktop debug callback symbols. +- 2026-06-12: DEBT-0058 and DEBT-0063 were narrowed. App-level + progress/message/input dialogs now route through a pure + `pp::app::AppDialogFactory`; retained `NodeProgressBar`, `NodeMessageBox`, + and `NodeInputBox` construction and root attachment are centralized behind + the legacy dialog/overlay factory layer. +- 2026-06-12: DEBT-0017, DEBT-0051, and DEBT-0055 were narrowed. Apple + document browse roots, file/image/save/directory picker dispatch, macOS + empty-selection filtering, working-directory picker policy, and display-path + formatting now live in `src/platform_apple/apple_platform_services.*`; the + retained legacy platform adapter delegates Apple document-platform calls to + that boundary while `App` still stores Apple platform handles directly. +- 2026-06-12: DEBT-0010, DEBT-0036, and DEBT-0043 were narrowed. Payload-complete + depth export now generates deterministic image/depth PNG payloads through + pure `pp_paint_renderer` and writes them through the app-core two-payload + writer before retained fallback. Retained OpenGL depth rendering/readback is + now fallback-only for unsupported targets, incomplete readback, or writer + failure; the pure path currently uses the extracted fixed perspective export + view rather than captured live legacy camera state. - 2026-06-12: DEBT-0062 was closed. The generated VS 2026 fmt overlay was removed from root CMake, reused build trees delete the stale - `compat/fmt-vs2026` directory, and VS 2026 consumers now compile retained fmt - with `_SECURE_SCL` undefined so the removed `stdext::checked_array_iterator` - path is not selected. + `compat/fmt-vs2026` directory, and VS 2026 retained fmt sources now use a + generated forced-include compatibility header that includes the STL + configuration once and undefines `_SECURE_SCL` before fmt selects its removed + checked-array-iterator branch. - 2026-06-12: DEBT-0039, DEBT-0040, and DEBT-0042 were narrowed. Document-open, close, save, save-before-workflow, Save As, Save Version, and new-document history effects now surface as explicit `pp_app_core` history outputs; the @@ -900,7 +924,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0007 | Open | Modernization | `vcpkg.json` and `windows-msvc-vcpkg-headless` are validated for the headless Windows component matrix, and root CMake now exposes a focused `panopainter_platform_build_vcpkg_ui_core` target for the vcpkg-backed `pp_ui_core`/tinyxml2 boundary, but app targets still use vendored libraries and Android/Apple triplets are not proven | Dependency migration must stay incremental while SDK/patched/vendor dependencies remain in use | `cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_platform_build_vcpkg_ui_core` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions | | DEBT-0008 | Open | Modernization | `windows-msvc-default` and `windows-msvc-vcpkg-headless` explicitly select Visual Studio 18 2026 for local validation, but non-VS2026 CMake executables on PATH may not know that generator | The local machine has VS 2026, but using an older CMake can still default to Ninja or reject the VS 2026 generator | `cmake --preset windows-msvc-default`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter`; `ctest --preset desktop-fast --build-config Debug` | The repo automation invokes or locates a CMake executable that supports `Visual Studio 18 2026`, and VS 2026 generator validation is the normal Windows path without manual tool selection | | DEBT-0009 | Open | Modernization | Android root CMake validation currently builds headless targets only, while retained standard/Quest/Focus package CMake paths now have a refreshed CMake 3.10/C++23 baseline outside root CMake; automation queries `sdkmanager`, installs newer or missing SDK Manager NDK/CMake packages, selects the resulting pair before configure, and reports update decisions; root CMake exposes non-default platform-build and retained native package validation targets | Platform app entrypoints still live in legacy Gradle/CMake projects and need Phase 6 alignment | `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64`; `cmake --build --preset android-x64`; `cmake --build --preset android-quest-arm64`; `cmake --build --preset android-focus-arm64`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_platform_build_android_assets`; `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard`; `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages quest,focus -ConfigureOnly`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly -AndroidNativeChecks -PackageKinds android-standard-apk,android-quest-apk,android-focus-apk`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_native_package_smoke` | Android standard, Quest, and Focus/Wave package targets consume shared component targets and have package smoke commands | -| DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document/undo-history model with alpha-lock metadata, snapshot construction, per-layer frame metadata, renderer-free RGBA8 face payload storage, snapshot-embedded face-payload validation, renderer-free alpha8 selection-mask storage, PPI import/export helpers, stroke-script-to-face-payload CLI automation, `pp_paint_renderer` document face/frame compositors, renderer-neutral six-face texture upload, pure six-face PNG export, pure equirectangular PNG export, pure equirectangular JPEG+XMP export, pure layer/animation-frame PNG collection export, shared document-frame export readiness reporting, depth export render-plan reporting, OpenGL command-planner validation through CLI render automation, live Canvas snapshot projection through `pp_app_core`/`legacy_document_canvas_services`, captured RGBA8 payload attachment to `pp_document`, live Save/Save As/Save Version/save-before-workflow snapshot-readiness reporting before retained save execution, pure app-core PPI export for payload-complete canvas snapshots, payload-complete canvas-snapshot renderer-upload plus face-PNG export automation, live cube-face face-PNG writer execution using app-core face target planning and write/publish service dispatch with retained fallback, live PNG/JPEG equirectangular writer execution using the paint-renderer equirectangular exports plus app-core file write/publish dispatch with retained fallback, live payload-complete layer/animation-frame collection writer execution using paint-renderer PNG sequences and app-core collection write/publish dispatch with retained fallback, tested app-core document-snapshot export route policy for writer versus retained fallback including current-platform support, and live equirectangular/layer/animation-frame/depth/cube-face export snapshot/render/export-readiness reporting through the shared readiness helper plus the depth render plan, but action-command adoption, live save-writer replacement, Web and incomplete-readback collection handoff, progress/threading parity, broader renderer-owned export execution, exact GPU/golden parity, depth render/readback replacement, and renderer-owned cube-face readback ownership are not yet wired | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2`; `pano_cli load-project --path tests\data\projects\minimal-project.ppi`; `pano_cli simulate-document-render --width 64 --height 32`; `pano_cli plan-canvas-document-snapshot --width 64 --height 32`; `pano_cli plan-canvas-document-snapshot --captured-face-payloads-per-layer 1`; `pano_cli plan-export-snapshot-route --kind layers-collection --captured-face-payloads 3 --pending-face-payloads 6`; `pp_document_tests`; `pp_document_ppi_import_tests`; `pp_document_ppi_export_tests`; `pp_paint_renderer_compositor_tests`; `pp_app_core_document_canvas_tests`; `pp_app_core_document_export_tests`; `pano_cli_simulate_document_edits_smoke`; `pano_cli_simulate_document_export_smoke`; `pano_cli_simulate_document_render_smoke`; `pano_cli_plan_canvas_document_snapshot_smoke`; `pano_cli_plan_canvas_document_snapshot_payload_smoke`; `pano_cli_plan_export_snapshot_route_pending_smoke`; `pano_cli_save_document_project_roundtrip_smoke`; `pano_cli_apply_stroke_script_roundtrip_smoke`; `pano_cli_apply_stroke_script_rejects_tiny_canvas` | Legacy document behavior is represented by `pp_document`/`pp_paint_renderer` tests and the app consumes it through a boundary/facade | +| DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document/undo-history model with alpha-lock metadata, snapshot construction, per-layer frame metadata, renderer-free RGBA8 face payload storage, snapshot-embedded face-payload validation, renderer-free alpha8 selection-mask storage, PPI import/export helpers, stroke-script-to-face-payload CLI automation, `pp_paint_renderer` document face/frame compositors, renderer-neutral six-face texture upload, pure six-face PNG export, pure equirectangular PNG export, pure equirectangular JPEG+XMP export, pure layer/animation-frame PNG collection export, pure depth image/depth PNG export for payload-complete snapshots, shared document-frame export readiness reporting, depth export render-plan reporting, OpenGL command-planner validation through CLI render automation, live Canvas snapshot projection through `pp_app_core`/`legacy_document_canvas_services`, captured RGBA8 payload attachment to `pp_document`, live Save/Save As/Save Version/save-before-workflow snapshot-readiness reporting before retained save execution, pure app-core PPI export for payload-complete canvas snapshots, payload-complete canvas-snapshot renderer-upload plus face-PNG export automation, live cube-face face-PNG writer execution using app-core face target planning and write/publish service dispatch with retained fallback, live PNG/JPEG equirectangular writer execution using the paint-renderer equirectangular exports plus app-core file write/publish dispatch with retained fallback, live payload-complete layer/animation-frame collection writer execution using paint-renderer PNG sequences and app-core collection write/publish dispatch with retained fallback, live payload-complete depth export execution using pure paint-renderer PNG payloads plus app-core two-payload writing with retained fallback, tested app-core document-snapshot export route policy for writer versus retained fallback including current-platform support, and live equirectangular/layer/animation-frame/depth/cube-face export snapshot/render/export-readiness reporting through the shared readiness helper plus the depth render plan, but action-command adoption, live save-writer replacement, Web and incomplete-readback collection handoff, progress/threading parity, broader renderer-owned export execution, exact GPU/golden parity, live-camera depth parity, and renderer-owned cube-face readback ownership are not yet wired | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2`; `pano_cli load-project --path tests\data\projects\minimal-project.ppi`; `pano_cli simulate-document-render --width 64 --height 32`; `pano_cli plan-canvas-document-snapshot --width 64 --height 32`; `pano_cli plan-canvas-document-snapshot --captured-face-payloads-per-layer 1`; `pano_cli plan-export-snapshot-route --kind layers-collection --captured-face-payloads 3 --pending-face-payloads 6`; `pp_document_tests`; `pp_document_ppi_import_tests`; `pp_document_ppi_export_tests`; `pp_paint_renderer_compositor_tests`; `pp_app_core_document_canvas_tests`; `pp_app_core_document_export_tests`; `pano_cli_simulate_document_edits_smoke`; `pano_cli_simulate_document_export_smoke`; `pano_cli_simulate_document_render_smoke`; `pano_cli_plan_canvas_document_snapshot_smoke`; `pano_cli_plan_canvas_document_snapshot_payload_smoke`; `pano_cli_plan_export_snapshot_route_pending_smoke`; `pano_cli_save_document_project_roundtrip_smoke`; `pano_cli_apply_stroke_script_roundtrip_smoke`; `pano_cli_apply_stroke_script_rejects_tiny_canvas` | Legacy document behavior is represented by `pp_document`/`pp_paint_renderer` tests and the app consumes it through a boundary/facade | | DEBT-0011 | Open | Modernization | `package-smoke` validates the Windows CMake app artifact and launch-folder DLL payload, and reports a structured package readiness matrix for Windows AppX, Android standard/Quest/Focus APKs, Apple bundles, Linux app output, and WebGL output; the Windows app smoke passes the configure-time CMake executable so VS 2026 generator validation does not depend on `cmake` from PATH, retained Android package native CMake paths, and retained Linux/WebGL CMake baseline metadata are reachable from package validation and root CMake package-readiness targets, but Windows AppX/APK/Linux/Apple/WebGL package outputs are still `blocked` because root CMake package targets do not exist yet | Platform package targets are not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_windows_app_package_smoke`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly -AndroidNativeChecks -PackageKinds android-standard-apk,android-quest-apk,android-focus-apk`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_native_package_smoke`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_linux_webgl_package_readiness`; `python scripts/dev/check_package_smoke_readiness.py`; `bash -n scripts/automation/package-smoke.sh` | Package-smoke builds and validates Windows AppX, Android APK variants, Linux app, Apple bundles, and WebGL output where local toolchains are present | | DEBT-0012 | Open | Modernization | `pp_ui_core` uses vcpkg tinyxml2 on `windows-msvc-vcpkg-headless`, but retains `pp_vendor_tinyxml2` for default and unproven platform presets | Mobile/AppX/Apple triplets and app packaging still need validation before removing the vendored fallback | `ctest --preset desktop-fast-vcpkg --build-config Debug`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | All supported presets consume vcpkg tinyxml2 or document a permanent vendored exception | | DEBT-0013 | Open | Modernization | `pp_assets`, `pp_document`, `pano_cli inspect-project`, `pano_cli load-project`, and `pano_cli save-project` validate the fixed PPI header, thumbnail/body byte layout, generated multi-layer/multi-frame PPI writing with explicit layer opacity/blend/alpha-lock/visibility metadata, per-layer frame durations, metadata-only and targeted dirty-face-payload save/load round-trips, layer/frame index, dirty-face descriptors, dirty-face PNG payload metadata, asset-level RGBA PNG payload decoding, pure document-to-PPI export, CLI document export automation, file-writing document export automation, stroke-script-generated document payload export, decoded pixel attachment to `pp_document`, live save-path snapshot-readiness reporting, and app-core canvas-snapshot-to-PPI export automation, but full legacy PPI round-trip parity and pure live save writer replacement are not yet extracted | Full PPI save parity requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_image_pixels_tests`; `pp_assets_ppi_header_tests`; `pp_document_ppi_import_tests`; `pp_document_ppi_export_tests`; `pano_cli_inspect_project_layout_smoke`; `pano_cli_load_project_metadata_smoke`; `pano_cli_save_project_roundtrip_smoke`; `pano_cli_save_project_payload_roundtrip_smoke`; `pano_cli_simulate_document_export_smoke`; `pano_cli_save_document_project_roundtrip_smoke`; `pano_cli_apply_stroke_script_roundtrip_smoke`; `pano_cli_apply_stroke_script_rejects_tiny_canvas` | Full PPI load/save fixtures cover thumbnails, decoded layer face payloads attached to documents, frames, corrupt payloads, dirty-face payload saving, arbitrary legacy canvas payload/layout combinations, and legacy app round-trip compatibility | @@ -940,13 +964,13 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0048 | Open | Modernization | ABR/PPBR brush package import execution now consumes pure `pp_app_core` through document-open confirmation callbacks, `pano_cli plan-brush-package-import`, `BrushPackageImportServices`, and `src/legacy_brush_package_import_services.*`; imported brush tip/pattern target paths now consume `pp_assets::brush_package`, but the bridge still launches detached legacy `NodePanelBrushPreset::import_abr`/`import_ppbr` worker threads and depends on the legacy preset panel as the importer/storage owner | Preserve current brush import behavior while brush package parsing, preset storage, progress/error reporting, and UI refresh move toward asset/paint/UI services | `pp_assets_brush_package_tests`; `pp_app_core_brush_package_import_tests`; `pano_cli plan-brush-package-import --kind ppbr --path D:/Paint/Brushes/clouds.ppbr`; `pano_cli plan-brush-package-import --kind abr --path D:/Paint/Brushes/clouds.abr`; `pano_cli plan-brush-package-import --kind ppbr`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | ABR/PPBR parsing, preset creation/storage, import threading/progress, duplicate asset policy, and UI refresh are owned by injected brush asset/paint/UI services with document-open callbacks only confirming user intent | | DEBT-0049 | Open | Modernization | `pp_assets::validate_ppbr_header` intentionally preserves the legacy PPBR version check from `NodePanelBrushPreset::import_ppbr`, which accepts files when either major is `0` or minor is `1` instead of requiring exactly version `0.1` | Avoid rejecting existing brush packages before compatibility fixtures prove the stricter rule is safe | `pp_assets_brush_package_tests`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Add PPBR compatibility fixtures for accepted/rejected historical package versions, then require canonical `0.1` or an explicit supported-version matrix and update live import accordingly | | DEBT-0050 | Open | Modernization | iOS exported-image photo-library publishing and WebGL persistent-storage flushing now dispatch through platform service boundaries; the iOS/Web policy decision lives in tested `pp_platform_api::platform_policy`, and WebGL flushing now goes through injectable `pp::platform::WebPlatformServices`, but non-Windows execution still lives in retained fallback adapters and forwards to retained `save_image_library`/`webgl_sync` bridges | Preserve current iOS/Web export and save behavior while the Apple/Web platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; platform package smoke once Apple/Web root builds exist | Exported-image publishing and persistent-storage flushing are owned by injected Apple/Web `pp_platform_*` services with no legacy adapter branch | -| DEBT-0051 | Open | Modernization | Document browser search roots and Browse dialog working-directory picker visibility/path formatting now dispatch through `PlatformServices`; iOS Inbox roots and working-directory picker availability live in tested `pp_platform_api::platform_policy`, but macOS directory picker/display-path execution still lives in `src/platform_legacy/legacy_platform_services.*` | Preserve current iOS document import/browse and desktop browse picker behavior while Apple platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Apple package smoke once root Apple builds exist | Document browse roots and browse-directory picker/display formatting are owned by injected Apple and desktop `pp_platform_*` services with no legacy adapter branch | +| DEBT-0051 | Open | Modernization | Document browser search roots, Apple file/image/save/directory picker dispatch, Browse dialog working-directory picker visibility/path formatting, iOS Inbox roots, macOS empty-selection filtering, and macOS display-path formatting now dispatch through the tested `src/platform_apple/apple_platform_services.*` boundary consumed by `PlatformServices`; retained `src/platform_legacy/legacy_platform_services.*` still creates the Apple bridge and owns other non-Apple fallback behavior | Preserve current iOS document import/browse and desktop browse picker behavior while Apple platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Apple package smoke once root Apple builds exist | Document browse roots and browse-directory picker/display formatting are owned by injected Apple and desktop `pp_platform_*` services with no legacy adapter branch | | DEBT-0052 | Open | Modernization | Native UI/window state saving now dispatches through `PlatformServices`; Windows/macOS save policy lives in tested `pp_platform_api::platform_policy`, and Windows placement reads/writes now use `LegacyWindowPreferenceSnapshot` plus `src/legacy_preference_storage.*`, but macOS execution still lives in `src/platform_legacy/legacy_platform_services.*` and forwards to the retained Objective-C app bridge while Windows still stores placement through retained `Settings` behind the adapter | Preserve current Windows/macOS UI persistence while platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Windows app build; Apple package smoke once root Apple builds exist | UI/window state persistence is owned by injected platform services with no legacy adapter branch | | DEBT-0053 | Open | Modernization | Prepared-file writable target selection and prepared-file export-dialog policy now dispatch through platform service boundaries; iOS temporary-file and WebGL data-path target planning live in tested `pp_platform_api::platform_policy`, and WebGL prepared-file handoff now goes through injectable `pp::platform::WebPlatformServices`, but retained iOS/Web save/download handoff execution still lives in retained fallback adapters | Preserve mobile/Web export handoff behavior while platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Windows app build; Apple/Web package smoke once root package builds exist | Prepared-file target selection, export-dialog policy, and save/download handoff are owned by injected platform services with no legacy adapter branch | | DEBT-0054 | Open | Modernization | Layout XML file read/reload decisions now consume `pp_platform_api::plan_asset_file_load`; platform-family reload behavior lives in tested `pp_platform_api::platform_policy` and pure probed planning, but the live wrapper still performs direct `stat` probing for Windows/macOS mtime reload checks until platform storage/file-watch services exist | Preserve current layout hot-reload and mobile/Web single-load behavior while removing platform guards from the shared `LayoutManager` parser | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests`; Windows app build | Layout reload decisions are owned by injected platform storage/file-watch services or an asset manager boundary with platform-specific file watching removed from compile-time helpers | | DEBT-0055 | Open | Modernization | `src/app.h` now forward-declares retained iOS/macOS/Android/Linux/Web platform handles instead of including platform SDK headers, and full SDK includes are isolated in `src/platform_legacy/legacy_platform_services.cpp`, but the `App` singleton still stores those platform handles directly | Reduce central header platform coupling incrementally without rewriting non-Windows platform entrypoints before Phase 6 | Windows app build; Apple/Android/Linux/Web package smoke once platform root builds are active | Platform handles are owned by injected `pp_platform_*` shell state or services, and `App` has no platform SDK handle fields or platform conditional members | | DEBT-0056 | Open | Modernization | `src/asset.h` is now Android-SDK-free and uses opaque Android asset handles behind `Asset::set_android_asset_manager`, but retained `Asset` still owns a static Android asset-manager bridge and `src/asset.cpp` still performs Android `AAssetManager` reads directly; the current `android-arm64` root preset is headless and does not expose `pp_legacy_assets_io`, though the retained Android standard package `native-lib` now builds through its refreshed C++23 CMake path | Reduce legacy asset I/O header coupling without rewriting Android asset loading before the asset manager/storage boundary exists | Windows app build; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64 -Targets pp_assets`; `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard` | Android asset loading is owned by injected asset storage/platform services or `pp_assets` file providers, with no static Android asset manager on `Asset` | -| DEBT-0060 | Open | Modernization | Retained Android package CMake generates a patched `nanort.h` overlay in the build tree for `native-lib` instead of modifying the `libs/nanort` submodule | Current SDK Manager NDK/Clang rejects `TriangleSAHPred::operator=` assigning to a `const size_t` member, but the retained grid/lightmap path still includes `nanort` before that dependency is replaced or updated | `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard`; Quest/Focus retained package configure checks; Windows app build | Update/replace `nanort`, move grid/lightmap baking behind a component that owns its dependency, or retire the retained Android package CMake path so no generated vendor overlay is required | +| DEBT-0060 | Open | Modernization | Retained Android package CMake no longer generates a patched `nanort.h` overlay, and standard/Quest/Focus package paths include `libs/nanort` directly after the vendored `TriangleSAHPred` assignment issue was fixed locally; retained Android standard validation now gets past `nanort` but fails later because `legacy_gl_runtime_dispatch.h` assumes desktop GL debug callback symbols that Android/GLES does not expose | Current Android retained package validation still needs a non-desktop GL runtime dispatch guard before the package path can be treated as green | `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard`; Quest/Focus retained package configure checks; Windows app build | Retained Android standard package validation passes without generated vendor overlays, or the retained Android package CMake path is retired | | DEBT-0061 | Open | Modernization | Desktop XR runtime selection now lives in tested `pp_platform_api` policy and prefers OpenXR, but `WindowsPlatformServices` still reports OpenXR unavailable and reaches the retained OpenVR SDK bridge as a legacy fallback; Windows runtime deployment copies `openvr_api.dll` beside `PanoPainter.exe` until that fallback is removed | Preserve current desktop VR behavior while replacing OpenVR with OpenXR behind the platform/renderer boundary | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Add an OpenXR SDK/package target, implement desktop OpenXR startup/shutdown/pose/controller submission behind `pp_platform_vr` or `PlatformServices`, validate parity with mocked/runtime smoke coverage, and remove `libs/openvr` plus the OpenVR link/include paths from root CMake | | DEBT-0063 | Open | Modernization | The retained UI tree still exposes `Node* m_parent`, public `std::vector> m_children`, raw `find()` lookup results, `add_child()` allocation through `new`, callbacks/observers that take or capture raw `Node*`, and manual `destroy()`/`m_destroyed` semantics. `pp_ui_core` now owns a tested `NodeLifetimeTree` target model with checked node handles, scoped callback connections, subtree destruction, pointer/keyboard capture release, whole-tree clear for layout reload, and mutation-safe dispatch, plus a tested `UiOverlayLifetime` popup/dialog stack model. Retained app-dialog root insertion, app-menu popup template cloning/root attachment, quick/stroke/brush panel popup root attachment, combo-box popup insertion, Open/Browse delete-confirmation dialog insertion, popup tick decoration insertion, top-toolbar panel popup insertion, repeated retained popup activation flag setup, repeated retained popup close/release execution, popup tick-decoration close callback wiring, and popup-panel outside-click release/remove/callback dispatch are now centralized in `src/legacy_ui_overlay_services.*`, but retained `Node`/`NodePopupMenu`/`NodeDialog*` still have not adopted checked handles or scoped callback ownership | Preserve current UI behavior while making panel/dialog extraction safe instead of spreading lifetime hazards into the new architecture | `pp_ui_core_layout_xml_tests`; `pp_ui_core_node_lifetime_tests`; `pp_ui_core_overlay_lifetime_tests`; future `pp_panopainter_ui_dialog_lifetime_tests`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Retained `Node` and `pp_panopainter_ui` adopt checked node handles or equivalent non-owning references, scoped callback connection/disconnect semantics, mutation-safe event dispatch, parent/child invariants hidden behind APIs, and destroy-during-callback/capture-release/popup-close/layout-reload tests; retained `Node*` APIs are removed or isolated behind compatibility adapters | | DEBT-0057 | Open | Modernization | Default canvas allocation size now dispatches through `PlatformServices::default_canvas_resolution`, removing the `CANVAS_RES` platform macro from `src/canvas.h`; WebGL's retained 512 default now lives in tested `pp_platform_api` policy behind injectable `pp::platform::WebPlatformServices`, but the Web shell still reaches the default implementation through the retained fallback until a dedicated Web service is injected directly | Preserve WebGL memory behavior while moving canvas creation policy out of shared canvas headers and into the platform boundary | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests`; Windows app build; WebGL package smoke once root Web build exists | Default canvas resolution is owned by injected `pp_platform_*` services for every supported platform, with no WebGL branch in the legacy fallback | @@ -957,6 +981,6 @@ agent or engineer to remove them without reconstructing context from chat. | ID | Status | Owner | Item | Reason | Validation | Removal Condition | | --- | --- | --- | --- | --- | --- | --- | -| DEBT-0062 | Closed | Modernization | VS 2026 builds generated a patched fmt `format.h` overlay in the build tree for `pp_legacy_vendor` | VS 2026's STL no longer exposes the legacy checked-array iterator used by the old fmt release | Closed on 2026-06-12: VS-bundled CMake build of `PanoPainter` and `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure` | Closed on 2026-06-12: the generated overlay was removed, stale overlay directories are deleted during configure, and VS 2026 consumers compile retained fmt with `_SECURE_SCL` undefined so `stdext::checked_array_iterator` is not selected | +| DEBT-0062 | Closed | Modernization | VS 2026 builds generated a patched fmt `format.h` overlay in the build tree for `pp_legacy_vendor` | VS 2026's STL no longer exposes the legacy checked-array iterator used by the old fmt release | Closed on 2026-06-12: VS-bundled CMake build of `PanoPainter` and `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure` | Closed on 2026-06-12: the generated overlay was removed, stale overlay directories are deleted during configure, and VS 2026 retained fmt sources use a generated forced-include compatibility header to keep fmt out of the removed checked-array-iterator branch | | DEBT-0006 | Closed | Modernization | `pano_cli create-document` validates and emits JSON command contracts but does not yet invoke the legacy document/app model | The document model had not been extracted from `Canvas`/`App` yet | `ctest --preset desktop-fast --build-config Debug`; `pano_cli_create_document_smoke` | Closed on 2026-05-31: command now constructs a real `pp_document::CanvasDocument` | | DEBT-0018 | Closed | Modernization | `pp_renderer_gl` owned a tested `OpenGlInitialState` plan for PanoPainter startup depth/blend policy, but `App::init` still executed the plan through direct OpenGL calls | Preserve behavior while moving renderer policy into the backend boundary before a live `IRenderDevice`/command context owns startup execution | `pp_renderer_gl_capabilities_tests`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Closed on 2026-06-03: `pp_renderer_gl::apply_panopainter_initial_state` now applies the startup state through a tested backend dispatch contract consumed by `App::init` | diff --git a/docs/modernization/tasks.md b/docs/modernization/tasks.md index c7b9b73..15ef298 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -34,11 +34,11 @@ auditable steps rather than by subjective estimates. | Build and CMake ownership | 15 | 12 | Root CMake owns active source lists, app/tool targets, and retained package entrypoints. | | Test and automation coverage | 15 | 9 | Headless, platform, package, and focused validation commands exist and are current. | | Pure component behavior ownership | 15 | 8 | Behavior lives in `pp_*` components and is consumed by live adapters. | -| Legacy adapter retirement | 20 | 5 | `legacy_*_services` and singleton bridges are deleted or reduced to trivial composition. | -| Renderer boundary and OpenGL parity | 15 | 3 | Live render/export/readback paths execute through renderer interfaces with parity checks. | -| Platform and package parity | 10 | 4 | Required platforms have root CMake/package validation and injected platform services. | +| Legacy adapter retirement | 20 | 7 | `legacy_*_services` and singleton bridges are deleted or reduced to trivial composition. | +| Renderer boundary and OpenGL parity | 15 | 10 | Live render/export/readback paths execute through renderer interfaces with parity checks. | +| Platform and package parity | 10 | 6 | Required platforms have root CMake/package validation and injected platform services. | | Hardening and future backend readiness | 10 | 0 | Edge, fuzz, golden, stress, and backend-lab gates exist for high-risk paths. | -| **Total** | **100** | **41** | Only completed tasks below may change this number. | +| **Total** | **100** | **52** | Only completed tasks below may change this number. | When updating `Current`, add a dated note under "Completed Task Log" with the task id, points moved, validation command, and commit hash. @@ -237,7 +237,7 @@ cmake --build --preset windows-msvc-default --config Debug --target PanoPainter ### RND-001 - Make Pure Equirectangular Export The Primary Success Path -Status: Ready +Status: Done Score: +2 renderer boundary and OpenGL parity Debt: `DEBT-0010`, `DEBT-0036`, `DEBT-0043` Scope: `src/legacy_document_export_services.*`, @@ -270,7 +270,7 @@ cmake --build --preset windows-msvc-default --config Debug --target PanoPainter ### RND-002 - Make Pure Layer And Animation Collection Export Primary -Status: Ready +Status: Done Score: +2 renderer boundary and OpenGL parity Debt: `DEBT-0010`, `DEBT-0036`, `DEBT-0043` Scope: `src/legacy_document_export_services.*`, @@ -299,7 +299,7 @@ cmake --build --preset windows-msvc-default --config Debug --target PanoPainter ### RND-003 - Replace Depth Export Readiness With Pure Depth Export Execution -Status: Ready +Status: Done Score: +3 renderer boundary and OpenGL parity Debt: `DEBT-0010`, `DEBT-0036`, `DEBT-0043` Scope: `src/paint_renderer/compositor.*`, @@ -331,7 +331,7 @@ cmake --build --preset windows-msvc-default --config Debug --target PanoPainter ### RND-004 - Add First Desktop GPU Golden Gate -Status: Ready +Status: Blocked Score: +2 hardening and future backend readiness Debt: `DEBT-0036` Scope: `tests/`, `CMakeLists.txt`, renderer test helpers only @@ -443,6 +443,9 @@ Score: +1 build and CMake ownership Debt: `DEBT-0060` Scope: retained Android package CMake, `libs/nanort`, grid/lightmap dependency wiring +Blocked By: Retained Android standard package now gets past `nanort`, but +fails compiling `legacy_gl_runtime_dispatch.h` on Android/GLES because +`glDebugMessageCallback` and `GLDEBUGPROC` are unavailable. Goal: @@ -527,7 +530,12 @@ Done Checks: | Date | Task | Score Change | Validation | Commit | | --- | --- | ---: | --- | --- | -| 2026-06-12 | DEP-001 | +1 build and CMake ownership | VS-bundled CMake build of `PanoPainter` and `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure` | 85d3fd5b | +| 2026-06-12 | RND-002 | +2 renderer boundary and OpenGL parity | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_export\|pp_paint_renderer_compositor\|pano_cli_plan_export_snapshot_route\|pano_cli_simulate_document_export" --output-on-failure` | pending | +| 2026-06-12 | RND-001 | +2 renderer boundary and OpenGL parity | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_export\|pp_paint_renderer_compositor\|pano_cli_plan_export_snapshot_route\|pano_cli_simulate_document_export" --output-on-failure` | pending | +| 2026-06-12 | ADP-004 | +2 legacy adapter retirement | VS-bundled CMake build of `pp_app_core_app_dialog_tests` and `pano_cli`; `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_app_dialog\|pano_cli_plan_app_dialog" --output-on-failure` | pending | +| 2026-06-12 | PLT-001 | +2 platform and package parity | VS-bundled CMake build of `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure`; Apple remote build blocked by unpublished fmt submodule pointer before DEP-001 correction | pending | +| 2026-06-12 | RND-003 | +3 renderer boundary and OpenGL parity | VS-bundled CMake build of `pp_paint_renderer_compositor_tests`, `pp_app_core_document_export_tests`, and `pano_cli`; `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_export\|pp_paint_renderer_compositor\|pano_cli_plan_export_snapshot_route" --output-on-failure` | pending | +| 2026-06-12 | DEP-001 | +1 build and CMake ownership | VS-bundled CMake build of `PanoPainter` and `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure` | 90f5fb29 | | 2026-06-12 | ADP-003 | +1 legacy adapter retirement | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_route\|pp_app_core_document_session\|pano_cli_plan_open_route\|pano_cli_simulate_app_session\|pano_cli_plan_document_file\|pano_cli_plan_document_version" --output-on-failure` | 34a9e910 | | 2026-06-12 | PLT-002 | +2 platform and package parity | `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly`; `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_layer\|pano_cli_plan_layer\|pp_platform_api_tests" --output-on-failure` | 8cd38401 | | 2026-06-12 | ADP-002 | +1 legacy adapter retirement | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_layer\|pano_cli_plan_layer" --output-on-failure`; `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_layer\|pano_cli_plan_layer\|pp_platform_api_tests" --output-on-failure` | ae242852 | diff --git a/src/app_core/app_dialog.h b/src/app_core/app_dialog.h index aa3eaf0..a22f337 100644 --- a/src/app_core/app_dialog.h +++ b/src/app_core/app_dialog.h @@ -2,6 +2,7 @@ #include "foundation/result.h" +#include #include #include @@ -34,6 +35,41 @@ struct AppInputDialogPlan { std::string ok_caption = "Ok"; }; +class AppDialog { +public: + virtual ~AppDialog() = default; + [[nodiscard]] virtual AppDialogKind kind() const noexcept = 0; +}; + +class AppProgressDialog : public AppDialog { +public: + ~AppProgressDialog() override = default; +}; + +class AppMessageDialog : public AppDialog { +public: + ~AppMessageDialog() override = default; +}; + +class AppInputDialog : public AppDialog { +public: + ~AppInputDialog() override = default; +}; + +class AppDialogFactory { +public: + virtual ~AppDialogFactory() = default; + + [[nodiscard]] virtual std::shared_ptr show_progress_dialog( + const AppProgressDialogPlan& plan) = 0; + + [[nodiscard]] virtual std::shared_ptr show_message_dialog( + const AppMessageDialogPlan& plan) = 0; + + [[nodiscard]] virtual std::shared_ptr show_input_dialog( + const AppInputDialogPlan& plan) = 0; +}; + [[nodiscard]] inline AppProgressDialogPlan plan_app_progress_dialog( std::string_view title, int total) noexcept diff --git a/src/app_core/document_export.h b/src/app_core/document_export.h index c749216..e4805a9 100644 --- a/src/app_core/document_export.h +++ b/src/app_core/document_export.h @@ -492,9 +492,9 @@ public: case DocumentExportExecutionKind::layers_stem: case DocumentExportExecutionKind::animation_frames_collection: case DocumentExportExecutionKind::animation_frames_stem: + case DocumentExportExecutionKind::depth: case DocumentExportExecutionKind::cube_faces: return true; - case DocumentExportExecutionKind::depth: case DocumentExportExecutionKind::animation_mp4: case DocumentExportExecutionKind::timelapse: return false; diff --git a/src/app_dialogs.cpp b/src/app_dialogs.cpp index 9bfc103..61ca2b5 100644 --- a/src/app_dialogs.cpp +++ b/src/app_dialogs.cpp @@ -126,13 +126,15 @@ void start_document_export_collection( std::shared_ptr App::show_progress(const std::string& title, int total /*= 0*/) { const auto plan = pp::app::plan_app_progress_dialog(title, total); - return pp::panopainter::create_legacy_app_progress_dialog(*this, plan); + const auto dialogs = pp::panopainter::make_legacy_app_dialog_factory(*this); + return pp::panopainter::legacy_progress_dialog_node(dialogs->show_progress_dialog(plan)); } std::shared_ptr App::message_box(const std::string &title, const std::string& text, bool cancel_button) { const auto plan = pp::app::plan_app_message_dialog(title, text, cancel_button); - return pp::panopainter::create_legacy_app_message_dialog(*this, plan); + const auto dialogs = pp::panopainter::make_legacy_app_dialog_factory(*this); + return pp::panopainter::legacy_message_dialog_node(dialogs->show_message_dialog(plan)); } std::shared_ptr App::input_box(const std::string& title, @@ -143,7 +145,8 @@ std::shared_ptr App::input_box(const std::string& title, LOG("input dialog skipped: %s", plan_result.status().message); return nullptr; } - return pp::panopainter::create_legacy_app_input_dialog(*this, plan_result.value()); + const auto dialogs = pp::panopainter::make_legacy_app_dialog_factory(*this); + return pp::panopainter::legacy_input_dialog_node(dialogs->show_input_dialog(plan_result.value())); } void App::dialog_usermanual() diff --git a/src/legacy_app_dialog_services.cpp b/src/legacy_app_dialog_services.cpp index 40ca1bc..1097f59 100644 --- a/src/legacy_app_dialog_services.cpp +++ b/src/legacy_app_dialog_services.cpp @@ -8,46 +8,150 @@ #include "node_progress_bar.h" namespace pp::panopainter { +namespace { + +class LegacyAppProgressDialog final : public pp::app::AppProgressDialog { +public: + explicit LegacyAppProgressDialog(std::shared_ptr node) noexcept + : node_(std::move(node)) + { + } + + [[nodiscard]] pp::app::AppDialogKind kind() const noexcept override + { + return pp::app::AppDialogKind::progress; + } + + [[nodiscard]] std::shared_ptr node() const noexcept + { + return node_; + } + +private: + std::shared_ptr node_; +}; + +class LegacyAppMessageDialog final : public pp::app::AppMessageDialog { +public: + explicit LegacyAppMessageDialog(std::shared_ptr node) noexcept + : node_(std::move(node)) + { + } + + [[nodiscard]] pp::app::AppDialogKind kind() const noexcept override + { + return pp::app::AppDialogKind::message; + } + + [[nodiscard]] std::shared_ptr node() const noexcept + { + return node_; + } + +private: + std::shared_ptr node_; +}; + +class LegacyAppInputDialog final : public pp::app::AppInputDialog { +public: + explicit LegacyAppInputDialog(std::shared_ptr node) noexcept + : node_(std::move(node)) + { + } + + [[nodiscard]] pp::app::AppDialogKind kind() const noexcept override + { + return pp::app::AppDialogKind::input; + } + + [[nodiscard]] std::shared_ptr node() const noexcept + { + return node_; + } + +private: + std::shared_ptr node_; +}; + +class LegacyAppDialogFactory final : public pp::app::AppDialogFactory { +public: + explicit LegacyAppDialogFactory(App& app) noexcept + : app_(app) + { + } + + [[nodiscard]] std::shared_ptr show_progress_dialog( + const pp::app::AppProgressDialogPlan& plan) override + { + return std::make_shared( + create_legacy_progress_dialog_overlay(app_, plan)); + } + + [[nodiscard]] std::shared_ptr show_message_dialog( + const pp::app::AppMessageDialogPlan& plan) override + { + return std::make_shared( + create_legacy_message_dialog_overlay(app_, plan)); + } + + [[nodiscard]] std::shared_ptr show_input_dialog( + const pp::app::AppInputDialogPlan& plan) override + { + return std::make_shared( + create_legacy_input_dialog_overlay(app_, plan)); + } + +private: + App& app_; +}; + +} // namespace + +std::unique_ptr make_legacy_app_dialog_factory(App& app) +{ + return std::make_unique(app); +} std::shared_ptr create_legacy_app_progress_dialog( App& app, const pp::app::AppProgressDialogPlan& plan) { - auto progress = make_legacy_overlay_node(app); - progress->m_progress->SetWidthP(plan.progress_fraction); - progress->m_title->set_text(plan.title.c_str()); - progress->m_total = plan.total; - progress->m_count = plan.count; - (void)attach_legacy_overlay_node(app, progress); - return progress; + return legacy_progress_dialog_node(make_legacy_app_dialog_factory(app)->show_progress_dialog(plan)); } std::shared_ptr create_legacy_app_message_dialog( App& app, const pp::app::AppMessageDialogPlan& plan) { - auto message = make_legacy_overlay_node(app); - message->m_title->set_text(plan.title.c_str()); - message->m_message->set_text(plan.message.c_str()); - message->btn_ok->m_text->set_text(plan.ok_caption.c_str()); - if (plan.show_cancel) - message->btn_cancel->m_text->set_text(plan.cancel_caption.c_str()); - else - close_legacy_dialog_node(*message->btn_cancel); - (void)attach_legacy_overlay_node(app, message); - return message; + return legacy_message_dialog_node(make_legacy_app_dialog_factory(app)->show_message_dialog(plan)); } std::shared_ptr create_legacy_app_input_dialog( App& app, const pp::app::AppInputDialogPlan& plan) { - auto input = make_legacy_overlay_node(app); - input->m_title->set_text(plan.title.c_str()); - input->m_field_name->set_text(plan.field_name.c_str()); - input->btn_ok->m_text->set_text(plan.ok_caption.c_str()); - (void)attach_legacy_overlay_node(app, input); - return input; + return legacy_input_dialog_node(make_legacy_app_dialog_factory(app)->show_input_dialog(plan)); +} + +std::shared_ptr legacy_progress_dialog_node( + const std::shared_ptr& dialog) noexcept +{ + auto legacy = std::dynamic_pointer_cast(dialog); + return legacy ? legacy->node() : nullptr; +} + +std::shared_ptr legacy_message_dialog_node( + const std::shared_ptr& dialog) noexcept +{ + auto legacy = std::dynamic_pointer_cast(dialog); + return legacy ? legacy->node() : nullptr; +} + +std::shared_ptr legacy_input_dialog_node( + const std::shared_ptr& dialog) noexcept +{ + auto legacy = std::dynamic_pointer_cast(dialog); + return legacy ? legacy->node() : nullptr; } } // namespace pp::panopainter diff --git a/src/legacy_app_dialog_services.h b/src/legacy_app_dialog_services.h index e5fef29..ab2d6bb 100644 --- a/src/legacy_app_dialog_services.h +++ b/src/legacy_app_dialog_services.h @@ -11,16 +11,27 @@ class NodeProgressBar; namespace pp::panopainter { -std::shared_ptr create_legacy_app_progress_dialog( +[[nodiscard]] std::unique_ptr make_legacy_app_dialog_factory(App& app); + +[[nodiscard]] std::shared_ptr create_legacy_app_progress_dialog( App& app, const pp::app::AppProgressDialogPlan& plan); -std::shared_ptr create_legacy_app_message_dialog( +[[nodiscard]] std::shared_ptr create_legacy_app_message_dialog( App& app, const pp::app::AppMessageDialogPlan& plan); -std::shared_ptr create_legacy_app_input_dialog( +[[nodiscard]] std::shared_ptr create_legacy_app_input_dialog( App& app, const pp::app::AppInputDialogPlan& plan); +[[nodiscard]] std::shared_ptr legacy_progress_dialog_node( + const std::shared_ptr& dialog) noexcept; + +[[nodiscard]] std::shared_ptr legacy_message_dialog_node( + const std::shared_ptr& dialog) noexcept; + +[[nodiscard]] std::shared_ptr legacy_input_dialog_node( + const std::shared_ptr& dialog) noexcept; + } // namespace pp::panopainter diff --git a/src/legacy_document_export_services.cpp b/src/legacy_document_export_services.cpp index 56537ef..3401fb8 100644 --- a/src/legacy_document_export_services.cpp +++ b/src/legacy_document_export_services.cpp @@ -330,6 +330,42 @@ pp::foundation::Status export_equirectangular_from_document_snapshot( services); } +pp::foundation::Status export_depth_from_document_snapshot( + App& app, + const pp::app::DocumentDepthExportTarget& target, + const LegacyDocumentExportSnapshotReports& reports) +{ + auto exported = pp::paint_renderer::export_document_depth_pngs( + pp::paint_renderer::DocumentDepthExportRenderPlanRequest { + .document = &reports.snapshot.document, + .frame_index = reports.snapshot.document.active_frame_index(), + }); + if (!exported) { + return exported.status(); + } + + LOG( + "export-depth document export PNG writer: %ux%u imageBytes=%llu depthBytes=%llu mergedFaceDraws=%zu layerDepthDraws=%zu visitedLayers=%zu visibleLayers=%zu facePayloads=%zu", + exported.value().output_extent.width, + exported.value().output_extent.height, + static_cast(exported.value().image_encoded_bytes), + static_cast(exported.value().depth_encoded_bytes), + exported.value().merged_face_draw_count, + exported.value().layer_depth_draw_count, + exported.value().visited_layer_count, + exported.value().visible_layer_count, + exported.value().face_payload_count); + + LegacyExportWriteServices services(app); + return pp::app::execute_document_depth_export_write( + target, + pp::app::DocumentDepthExportPayload { + .image_bytes = std::span(exported.value().image_png), + .depth_bytes = std::span(exported.value().depth_png), + }, + services); +} + class LegacyDocumentExportServices final : public pp::app::DocumentExportServices { public: explicit LegacyDocumentExportServices(App& app) noexcept @@ -565,15 +601,29 @@ public: const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-depth"); if (prepared) { - const auto report = pp::app::make_document_canvas_save_snapshot_report(prepared.value().snapshot); - const auto route = pp::app::plan_document_export_snapshot_route_for_current_platform( - pp::app::DocumentExportExecutionKind::depth, - report); - if (!route.uses_document_snapshot_writer) { - LOG( - "export-depth document export writer retained legacy export: %.*s", - static_cast(route.fallback_reason.size()), - route.fallback_reason.data()); + if (should_use_document_snapshot_writer( + "export-depth", + pp::app::DocumentExportExecutionKind::depth, + prepared.value(), + {})) { + if (target) { + const auto exported = export_depth_from_document_snapshot(app_, target.value(), prepared.value()); + if (exported.ok()) { + show_export_success_dialog( + app_, + pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::depth, + pp::app::document_export_media_platform_destination(), + app_.work_path)); + return; + } + + LOG("export-depth document export writer retained legacy export after failure: %s", exported.message); + } else { + LOG( + "export-depth document export writer retained legacy export after target failure: %s", + target.status().message); + } } #if !__WEB__ const auto plan = pp::paint_renderer::plan_document_depth_export_render( diff --git a/src/legacy_ui_overlay_services.cpp b/src/legacy_ui_overlay_services.cpp index 699b03c..9154e02 100644 --- a/src/legacy_ui_overlay_services.cpp +++ b/src/legacy_ui_overlay_services.cpp @@ -3,7 +3,10 @@ #include "app.h" #include "node.h" +#include "node_input_box.h" +#include "node_message_box.h" #include "node_popup_menu.h" +#include "node_progress_bar.h" namespace pp::panopainter { @@ -148,4 +151,45 @@ pp::foundation::Result> add_legacy_popup_menu( return pp::foundation::Result>::success(popup); } +std::shared_ptr create_legacy_progress_dialog_overlay( + App& app, + const pp::app::AppProgressDialogPlan& plan) +{ + auto progress = make_legacy_overlay_node(app); + progress->m_progress->SetWidthP(plan.progress_fraction); + progress->m_title->set_text(plan.title.c_str()); + progress->m_total = plan.total; + progress->m_count = plan.count; + (void)attach_legacy_overlay_node(app, progress); + return progress; +} + +std::shared_ptr create_legacy_message_dialog_overlay( + App& app, + const pp::app::AppMessageDialogPlan& plan) +{ + auto message = make_legacy_overlay_node(app); + message->m_title->set_text(plan.title.c_str()); + message->m_message->set_text(plan.message.c_str()); + message->btn_ok->m_text->set_text(plan.ok_caption.c_str()); + if (plan.show_cancel) + message->btn_cancel->m_text->set_text(plan.cancel_caption.c_str()); + else + close_legacy_dialog_node(*message->btn_cancel); + (void)attach_legacy_overlay_node(app, message); + return message; +} + +std::shared_ptr create_legacy_input_dialog_overlay( + App& app, + const pp::app::AppInputDialogPlan& plan) +{ + auto input = make_legacy_overlay_node(app); + input->m_title->set_text(plan.title.c_str()); + input->m_field_name->set_text(plan.field_name.c_str()); + input->btn_ok->m_text->set_text(plan.ok_caption.c_str()); + (void)attach_legacy_overlay_node(app, input); + return input; +} + } // namespace pp::panopainter diff --git a/src/legacy_ui_overlay_services.h b/src/legacy_ui_overlay_services.h index ad2c66f..d2bbe08 100644 --- a/src/legacy_ui_overlay_services.h +++ b/src/legacy_ui_overlay_services.h @@ -1,5 +1,6 @@ #pragma once +#include "app_core/app_dialog.h" #include "foundation/result.h" #include "node.h" @@ -7,7 +8,10 @@ #include class App; +class NodeInputBox; +class NodeMessageBox; class NodePopupMenu; +class NodeProgressBar; namespace pp::panopainter { @@ -40,6 +44,18 @@ void close_legacy_popup_panel( float y, float rtl_anchor_width) noexcept; +[[nodiscard]] std::shared_ptr create_legacy_progress_dialog_overlay( + App& app, + const pp::app::AppProgressDialogPlan& plan); + +[[nodiscard]] std::shared_ptr create_legacy_message_dialog_overlay( + App& app, + const pp::app::AppMessageDialogPlan& plan); + +[[nodiscard]] std::shared_ptr create_legacy_input_dialog_overlay( + App& app, + const pp::app::AppInputDialogPlan& plan); + template std::shared_ptr make_legacy_overlay_node(App& app) { diff --git a/src/paint_renderer/compositor.cpp b/src/paint_renderer/compositor.cpp index 48db1b1..41673c0 100644 --- a/src/paint_renderer/compositor.cpp +++ b/src/paint_renderer/compositor.cpp @@ -10,6 +10,12 @@ namespace pp::paint_renderer { +pp::foundation::Result composite_document_layer_frame( + const pp::document::CanvasDocument& document, + std::size_t layer_index, + std::size_t frame_index, + pp::paint::Rgba clear_color); + namespace { [[nodiscard]] bool is_valid_blend_mode(pp::paint::BlendMode mode) noexcept @@ -122,6 +128,8 @@ struct CubeFaceSample { float t = 0.0F; }; +constexpr float document_depth_export_default_fov_degrees = 85.0F; + [[nodiscard]] CubeFaceSample panopainter_cube_face_sample(float x, float y, float z) noexcept { const auto ax = std::fabs(x); @@ -279,6 +287,56 @@ struct EquirectangularProjectionResult { std::size_t composited_layer_face_count = 0; }; +struct PerspectiveProjectionMap { + pp::renderer::Extent2D output_extent {}; + std::vector samples; +}; + +pp::foundation::Result make_perspective_projection_map( + pp::renderer::Extent2D output_extent, + float vertical_fov_degrees) +{ + if (!std::isfinite(vertical_fov_degrees)) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("document depth export field of view must be finite")); + } + if (vertical_fov_degrees <= 0.0F || vertical_fov_degrees >= 180.0F) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("document depth export field of view must be between 0 and 180")); + } + + const auto output_pixel_count = expected_pixel_count(output_extent); + if (!output_pixel_count) { + return pp::foundation::Result::failure(output_pixel_count.status()); + } + + PerspectiveProjectionMap map; + map.output_extent = output_extent; + map.samples.reserve(output_pixel_count.value()); + + constexpr auto pi = 3.14159265358979323846F; + const auto tan_half_fov = std::tan(vertical_fov_degrees * pi / 360.0F); + const auto aspect = static_cast(output_extent.width) / static_cast(output_extent.height); + for (std::uint32_t y = 0; y < output_extent.height; ++y) { + const auto ny = 1.0F + - ((static_cast(y) + 0.5F) / static_cast(output_extent.height)) * 2.0F; + for (std::uint32_t x = 0; x < output_extent.width; ++x) { + const auto nx = ((static_cast(x) + 0.5F) / static_cast(output_extent.width)) * 2.0F + - 1.0F; + const auto dir_x = nx * aspect * tan_half_fov; + const auto dir_y = ny * tan_half_fov; + constexpr auto dir_z = -1.0F; + const auto inv_length = 1.0F / std::sqrt(dir_x * dir_x + dir_y * dir_y + dir_z * dir_z); + map.samples.push_back(panopainter_cube_face_sample( + dir_x * inv_length, + dir_y * inv_length, + dir_z * inv_length)); + } + } + + return pp::foundation::Result::success(std::move(map)); +} + pp::foundation::Result project_document_frame_equirectangular( const DocumentFrameCompositeResult& composite) { @@ -340,6 +398,87 @@ pp::foundation::Result project_document_frame_e return pp::foundation::Result::success(std::move(result)); } +pp::foundation::Result> project_document_frame_perspective( + const DocumentFrameCompositeResult& composite, + const PerspectiveProjectionMap& projection) +{ + const auto face_pixel_count = expected_pixel_count(composite.extent); + if (!face_pixel_count) { + return pp::foundation::Result>::failure(face_pixel_count.status()); + } + + for (const auto& face : composite.faces) { + if (face.extent.width != composite.extent.width || face.extent.height != composite.extent.height + || face.pixels.size() != face_pixel_count.value()) { + return pp::foundation::Result>::failure( + pp::foundation::Status::invalid_argument("document depth export requires complete cube faces")); + } + } + + std::vector pixels; + pixels.reserve(projection.samples.size()); + for (const auto& sample : projection.samples) { + pixels.push_back(sample_face_nearest(composite.faces[sample.face_index], sample.s, sample.t)); + } + + return pp::foundation::Result>::success(std::move(pixels)); +} + +float sample_face_alpha_nearest( + const DocumentFaceCompositeResult& face, + float s, + float t) noexcept +{ + return sample_face_nearest(face, s, t).a; +} + +pp::foundation::Result> project_document_depth_perspective( + const pp::document::CanvasDocument& document, + std::size_t frame_index, + const PerspectiveProjectionMap& projection) +{ + std::vector pixels( + projection.samples.size(), + pp::paint::Rgba { + .r = 0.0F, + .g = 0.0F, + .b = 0.0F, + .a = 1.0F, + }); + const auto layer_count = document.layers().size(); + for (std::size_t layer_index = 0; layer_index < layer_count; ++layer_index) { + const auto& layer = document.layers()[layer_index]; + if (!layer.visible || layer.opacity == 0.0F || frame_index >= layer.frames.size()) { + continue; + } + + auto composite = composite_document_layer_frame(document, layer_index, frame_index, {}); + if (!composite) { + return pp::foundation::Result>::failure(composite.status()); + } + + const auto gray = static_cast(layer_index + 1U) / static_cast(layer_count + 1U); + const pp::paint::Rgba layer_color { + .r = gray, + .g = gray, + .b = gray, + .a = 1.0F, + }; + for (std::size_t pixel_index = 0; pixel_index < projection.samples.size(); ++pixel_index) { + const auto& sample = projection.samples[pixel_index]; + const auto alpha = sample_face_alpha_nearest( + composite.value().faces[sample.face_index], + sample.s, + sample.t); + if (alpha > 0.01F) { + pixels[pixel_index] = layer_color; + } + } + } + + return pp::foundation::Result>::success(std::move(pixels)); +} + } pp::foundation::Status composite_layer( @@ -806,6 +945,79 @@ pp::foundation::Result plan_document_depth_export return pp::foundation::Result::success(plan); } +pp::foundation::Result export_document_depth_pngs( + DocumentDepthExportRenderPlanRequest request) +{ + auto plan = plan_document_depth_export_render(request); + if (!plan) { + return pp::foundation::Result::failure(plan.status()); + } + + auto projection = make_perspective_projection_map( + plan.value().output_extent, + document_depth_export_default_fov_degrees); + if (!projection) { + return pp::foundation::Result::failure(projection.status()); + } + + auto image_composite = composite_document_frame(DocumentFrameCompositeRequest { + .document = request.document, + .frame_index = request.frame_index, + .clear_color = pp::paint::Rgba { + .r = 0.0F, + .g = 0.0F, + .b = 0.0F, + .a = 1.0F, + }, + }); + if (!image_composite) { + return pp::foundation::Result::failure(image_composite.status()); + } + + auto image_pixels = project_document_frame_perspective(image_composite.value(), projection.value()); + if (!image_pixels) { + return pp::foundation::Result::failure(image_pixels.status()); + } + + auto depth_pixels = project_document_depth_perspective(*request.document, request.frame_index, projection.value()); + if (!depth_pixels) { + return pp::foundation::Result::failure(depth_pixels.status()); + } + + std::vector rgba8; + append_rgba8_bytes(rgba8, image_pixels.value()); + auto image_png = pp::assets::encode_png_rgba8( + plan.value().output_extent.width, + plan.value().output_extent.height, + rgba8); + if (!image_png) { + return pp::foundation::Result::failure(image_png.status()); + } + + append_rgba8_bytes(rgba8, depth_pixels.value()); + auto depth_png = pp::assets::encode_png_rgba8( + plan.value().output_extent.width, + plan.value().output_extent.height, + rgba8); + if (!depth_png) { + return pp::foundation::Result::failure(depth_png.status()); + } + + DocumentDepthPngExportResult result; + result.output_extent = plan.value().output_extent; + result.image_encoded_bytes = static_cast(image_png.value().size()); + result.depth_encoded_bytes = static_cast(depth_png.value().size()); + result.merged_face_draw_count = plan.value().merged_face_draw_count; + result.layer_depth_draw_count = plan.value().layer_depth_draw_count; + result.visited_layer_count = plan.value().visited_layer_count; + result.visible_layer_count = plan.value().visible_layer_count; + result.face_payload_count = plan.value().face_payload_count; + result.uses_perspective_camera = plan.value().uses_perspective_camera; + result.image_png = std::move(image_png.value()); + result.depth_png = std::move(depth_png.value()); + return pp::foundation::Result::success(std::move(result)); +} + pp::foundation::Result export_document_layers_equirectangular_pngs(DocumentLayerEquirectangularPngExportRequest request) { diff --git a/src/paint_renderer/compositor.h b/src/paint_renderer/compositor.h index 6e067aa..0bab26f 100644 --- a/src/paint_renderer/compositor.h +++ b/src/paint_renderer/compositor.h @@ -189,6 +189,23 @@ struct DocumentDepthExportRenderPlan { bool requires_renderer_readback = true; }; +struct DocumentDepthPngExportResult { + pp::renderer::Extent2D output_extent { + .width = 1024, + .height = 1024, + }; + std::vector image_png; + std::vector depth_png; + std::uint64_t image_encoded_bytes = 0; + std::uint64_t depth_encoded_bytes = 0; + std::size_t merged_face_draw_count = 0; + std::size_t layer_depth_draw_count = 0; + std::size_t visited_layer_count = 0; + std::size_t visible_layer_count = 0; + std::size_t face_payload_count = 0; + bool uses_perspective_camera = true; +}; + struct DocumentLayerEquirectangularPngExportRequest { const pp::document::CanvasDocument* document = nullptr; std::size_t frame_index = 0; @@ -278,6 +295,9 @@ export_document_frame_equirectangular_jpeg( [[nodiscard]] pp::foundation::Result plan_document_depth_export_render( DocumentDepthExportRenderPlanRequest request) noexcept; +[[nodiscard]] pp::foundation::Result export_document_depth_pngs( + DocumentDepthExportRenderPlanRequest request); + [[nodiscard]] pp::foundation::Result export_document_layers_equirectangular_pngs(DocumentLayerEquirectangularPngExportRequest request); diff --git a/src/platform_apple/apple_platform_services.cpp b/src/platform_apple/apple_platform_services.cpp new file mode 100644 index 0000000..a64f278 --- /dev/null +++ b/src/platform_apple/apple_platform_services.cpp @@ -0,0 +1,128 @@ +#include "platform_apple/apple_platform_services.h" + +#include "app_core/document_platform_io.h" +#include "platform_api/platform_policy.h" + +#include +#include + +namespace pp::platform::apple { +namespace { + +[[nodiscard]] std::vector apple_image_file_types() +{ + static constexpr std::array kFileTypes = { + "png", + "PNG", + "jpg", + "JPG", + "jpeg", + }; + + return { kFileTypes.begin(), kFileTypes.end() }; +} + +void invoke_picked_path_if_selected( + const std::string& path, + const PickedPathCallback& callback) +{ + if (pp::app::plan_picked_path(path) == pp::app::PickedPathAction::invoke_callback) + callback(path); +} + +} + +AppleDocumentPlatformServices::AppleDocumentPlatformServices( + PlatformFamily family, + AppleDocumentPickerBridge bridge) + : family_(family) + , bridge_(std::move(bridge)) +{ +} + +std::vector AppleDocumentPlatformServices::document_browse_roots( + std::string_view work_path, + std::string_view data_path) const +{ + return platform_document_browse_roots(family_, work_path, data_path); +} + +void AppleDocumentPlatformServices::pick_image(PickedPathCallback callback) const +{ + if (family_ == PlatformFamily::ios) + { + if (bridge_.pick_image) + bridge_.pick_image(std::move(callback)); + return; + } + + if (family_ == PlatformFamily::macos && bridge_.pick_file) + { + bridge_.pick_file( + apple_image_file_types(), + [callback = std::move(callback)](std::string path) { + invoke_picked_path_if_selected(path, callback); + }); + } +} + +void AppleDocumentPlatformServices::pick_file( + std::vector file_types, + PickedPathCallback callback) const +{ + if (!bridge_.pick_file) + return; + + if (family_ == PlatformFamily::ios) + { + bridge_.pick_file(std::move(file_types), std::move(callback)); + return; + } + + if (family_ == PlatformFamily::macos) + { + bridge_.pick_file( + std::move(file_types), + [callback = std::move(callback)](std::string path) { + invoke_picked_path_if_selected(path, callback); + }); + } +} + +void AppleDocumentPlatformServices::pick_save_file( + std::vector file_types, + PickedPathCallback callback) const +{ + if (family_ == PlatformFamily::macos && bridge_.pick_save_file) + { + bridge_.pick_save_file( + std::move(file_types), + [callback = std::move(callback)](std::string path) { + invoke_picked_path_if_selected(path, callback); + }); + } +} + +void AppleDocumentPlatformServices::pick_directory(PickedPathCallback callback) const +{ + if (family_ == PlatformFamily::macos && bridge_.pick_directory) + { + bridge_.pick_directory([callback = std::move(callback)](std::string path) { + invoke_picked_path_if_selected(path, callback); + }); + } +} + +bool AppleDocumentPlatformServices::supports_working_directory_picker() const +{ + return platform_supports_working_directory_picker(family_); +} + +std::string AppleDocumentPlatformServices::format_working_directory_path(std::string_view path) const +{ + if (family_ == PlatformFamily::macos && bridge_.format_working_directory_path) + return bridge_.format_working_directory_path(path); + return std::string(path); +} + +} diff --git a/src/platform_apple/apple_platform_services.h b/src/platform_apple/apple_platform_services.h new file mode 100644 index 0000000..0dcca10 --- /dev/null +++ b/src/platform_apple/apple_platform_services.h @@ -0,0 +1,44 @@ +#pragma once + +#include "platform_api/platform_policy.h" +#include "platform_api/platform_services.h" + +#include +#include +#include +#include + +namespace pp::platform::apple { + +struct AppleDocumentPickerBridge { + std::function pick_image; + std::function file_types, PickedPathCallback callback)> pick_file; + std::function file_types, PickedPathCallback callback)> pick_save_file; + std::function pick_directory; + std::function format_working_directory_path; +}; + +class AppleDocumentPlatformServices { +public: + explicit AppleDocumentPlatformServices( + PlatformFamily family, + AppleDocumentPickerBridge bridge = {}); + + [[nodiscard]] std::vector document_browse_roots( + std::string_view work_path, + std::string_view data_path) const; + + void pick_image(PickedPathCallback callback) const; + void pick_file(std::vector file_types, PickedPathCallback callback) const; + void pick_save_file(std::vector file_types, PickedPathCallback callback) const; + void pick_directory(PickedPathCallback callback) const; + + [[nodiscard]] bool supports_working_directory_picker() const; + [[nodiscard]] std::string format_working_directory_path(std::string_view path) const; + +private: + PlatformFamily family_; + AppleDocumentPickerBridge bridge_; +}; + +} diff --git a/src/platform_legacy/legacy_platform_services.cpp b/src/platform_legacy/legacy_platform_services.cpp index fb36957..08f9cbd 100644 --- a/src/platform_legacy/legacy_platform_services.cpp +++ b/src/platform_legacy/legacy_platform_services.cpp @@ -2,9 +2,9 @@ #include "platform_legacy/legacy_platform_services.h" #include "app.h" -#include "app_core/document_platform_io.h" #include "legacy_ui_gl_dispatch.h" #include "log.h" +#include "platform_apple/apple_platform_services.h" #include "platform_api/network_tls_policy.h" #include "platform_api/platform_policy.h" #include "renderer_gl/opengl_capabilities.h" @@ -52,14 +52,6 @@ void webgl_sync(); 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); -} - #ifdef __WEB__ class RetainedWebPlatformServices final : public pp::platform::WebPlatformServices { public: @@ -102,6 +94,79 @@ public: #endif } +#if defined(__IOS__) || defined(__OSX__) +[[nodiscard]] NSMutableArray* apple_file_types_array(const std::vector& file_types) +{ + NSMutableArray* types = [NSMutableArray arrayWithCapacity:file_types.size()]; + for (const auto& type : file_types) + { + [types addObject:[NSString stringWithCString:type.c_str() encoding:NSUTF8StringEncoding]]; + } + return types; +} + +[[nodiscard]] pp::platform::apple::AppleDocumentPlatformServices& active_apple_document_platform_services() +{ +#ifdef __IOS__ + static pp::platform::apple::AppleDocumentPlatformServices services( + pp::platform::PlatformFamily::ios, + [] { + pp::platform::apple::AppleDocumentPickerBridge bridge; + bridge.pick_image = [](pp::platform::PickedPathCallback callback) { + dispatch_async(dispatch_get_main_queue(), ^{ + [App::I->ios_view pick_photo:callback]; + }); + }; + bridge.pick_file = []( + std::vector file_types, + pp::platform::PickedPathCallback callback) { + dispatch_async(dispatch_get_main_queue(), ^{ + [App::I->ios_view pick_file:apple_file_types_array(file_types) then:callback]; + }); + }; + return bridge; + }()); + return services; +#else + static pp::platform::apple::AppleDocumentPlatformServices services( + pp::platform::PlatformFamily::macos, + [] { + pp::platform::apple::AppleDocumentPickerBridge bridge; + bridge.pick_file = []( + std::vector file_types, + pp::platform::PickedPathCallback callback) { + dispatch_async(dispatch_get_main_queue(), ^{ + const std::string path = [App::I->osx_view pick_file:apple_file_types_array(file_types)]; + callback(path); + }); + }; + bridge.pick_save_file = []( + std::vector file_types, + pp::platform::PickedPathCallback callback) { + dispatch_async(dispatch_get_main_queue(), ^{ + const std::string path = [App::I->osx_view pick_file_save:apple_file_types_array(file_types)]; + callback(path); + }); + }; + bridge.pick_directory = [](pp::platform::PickedPathCallback callback) { + dispatch_async(dispatch_get_main_queue(), ^{ + const std::string path = [App::I->osx_view pick_dir]; + callback(path); + }); + }; + bridge.format_working_directory_path = [](std::string_view path) { + char path_buffer[4096] = {}; + if (realpath(std::string(path).c_str(), path_buffer)) + return std::string(path_buffer); + return std::string(path); + }; + return bridge; + }()); + return services; +#endif +} +#endif + // DEBT-0017: fallback for platforms that do not inject PlatformServices yet. class LegacyPlatformServices final : public pp::platform::PlatformServices { public: @@ -376,10 +441,14 @@ public: std::string_view work_path, std::string_view data_path) override { +#if defined(__IOS__) || defined(__OSX__) + return active_apple_document_platform_services().document_browse_roots(work_path, data_path); +#else return pp::platform::platform_document_browse_roots( pp::platform::current_platform_family(), work_path, data_path); +#endif } void save_ui_state() override @@ -414,15 +483,9 @@ public: void pick_image(pp::platform::PickedPathCallback callback) override { #ifdef __IOS__ - dispatch_async(dispatch_get_main_queue(), ^{ - [App::I->ios_view pick_photo:callback]; - }); + active_apple_document_platform_services().pick_image(std::move(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); - }); + active_apple_document_platform_services().pick_image(std::move(callback)); #elif __ANDROID__ android_pick_file(callback); #elif __LINUX__ @@ -438,20 +501,9 @@ public: 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]; - }); + active_apple_document_platform_services().pick_file(std::move(file_types), std::move(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); - }); + active_apple_document_platform_services().pick_file(std::move(file_types), std::move(callback)); #elif __ANDROID__ android_pick_file(callback); #elif __LINUX__ @@ -468,13 +520,7 @@ public: 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); - }); + active_apple_document_platform_services().pick_save_file(std::move(file_types), std::move(callback)); #elif __ANDROID__ android_pick_file_save(callback); #else @@ -488,10 +534,7 @@ public: #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); - }); + active_apple_document_platform_services().pick_directory(std::move(callback)); #elif __ANDROID__ (void)callback; #else @@ -501,16 +544,18 @@ public: [[nodiscard]] bool supports_working_directory_picker() override { +#if defined(__IOS__) || defined(__OSX__) + return active_apple_document_platform_services().supports_working_directory_picker(); +#else return pp::platform::platform_supports_working_directory_picker( pp::platform::current_platform_family()); +#endif } [[nodiscard]] std::string format_working_directory_path(std::string_view path) override { -#if defined(__OSX__) - char path_buffer[4096] = {}; - if (realpath(std::string(path).c_str(), path_buffer)) - return path_buffer; +#if defined(__IOS__) || defined(__OSX__) + return active_apple_document_platform_services().format_working_directory_path(path); #endif return std::string(path); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 598e99b..ec802f7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -928,7 +928,7 @@ if(TARGET pano_cli) COMMAND pano_cli plan-export-snapshot-route --kind depth) set_tests_properties(pano_cli_plan_export_snapshot_route_depth_smoke PROPERTIES LABELS "app;integration;desktop-fast" - PASS_REGULAR_EXPRESSION "\"command\":\"plan-export-snapshot-route\".*\"kind\":\"depth\".*\"targetSupported\":false.*\"platformSupported\":true.*\"action\":\"use-legacy-export\".*\"fallbackReason\":\"document snapshot export does not support this target\"") + PASS_REGULAR_EXPRESSION "\"command\":\"plan-export-snapshot-route\".*\"kind\":\"depth\".*\"targetSupported\":true.*\"platformSupported\":true.*\"action\":\"use-document-snapshot-writer\".*\"usesDocumentSnapshotWriter\":true.*\"fallbackReason\":\"\"") add_test(NAME pano_cli_plan_export_menu_rejects_unknown COMMAND pano_cli plan-export-menu --kind unknown) diff --git a/tests/app_core/app_dialog_tests.cpp b/tests/app_core/app_dialog_tests.cpp index e6a57aa..e94472c 100644 --- a/tests/app_core/app_dialog_tests.cpp +++ b/tests/app_core/app_dialog_tests.cpp @@ -3,6 +3,51 @@ namespace { +class TestProgressDialog final : public pp::app::AppProgressDialog { +public: + [[nodiscard]] pp::app::AppDialogKind kind() const noexcept override + { + return pp::app::AppDialogKind::progress; + } +}; + +class TestMessageDialog final : public pp::app::AppMessageDialog { +public: + [[nodiscard]] pp::app::AppDialogKind kind() const noexcept override + { + return pp::app::AppDialogKind::message; + } +}; + +class TestInputDialog final : public pp::app::AppInputDialog { +public: + [[nodiscard]] pp::app::AppDialogKind kind() const noexcept override + { + return pp::app::AppDialogKind::input; + } +}; + +class TestDialogFactory final : public pp::app::AppDialogFactory { +public: + std::shared_ptr show_progress_dialog( + const pp::app::AppProgressDialogPlan&) override + { + return std::make_shared(); + } + + std::shared_ptr show_message_dialog( + const pp::app::AppMessageDialogPlan&) override + { + return std::make_shared(); + } + + std::shared_ptr show_input_dialog( + const pp::app::AppInputDialogPlan&) override + { + return std::make_shared(); + } +}; + void progress_dialog_initializes_progress_state(pp::tests::Harness& harness) { const auto plan = pp::app::plan_app_progress_dialog("Saving", 12); @@ -62,6 +107,14 @@ void input_dialog_rejects_empty_ok_caption(pp::tests::Harness& harness) PP_EXPECT(harness, plan.status().code == pp::foundation::StatusCode::invalid_argument); } +void dialog_factory_uses_pure_app_core_dialog_types(pp::tests::Harness& harness) +{ + TestDialogFactory factory; + PP_EXPECT(harness, factory.show_progress_dialog({})->kind() == pp::app::AppDialogKind::progress); + PP_EXPECT(harness, factory.show_message_dialog({})->kind() == pp::app::AppDialogKind::message); + PP_EXPECT(harness, factory.show_input_dialog({})->kind() == pp::app::AppDialogKind::input); +} + } int main() @@ -74,5 +127,6 @@ int main() harness.run("message dialog allows custom button captions", message_dialog_allows_custom_button_captions); harness.run("input dialog preserves ok caption", input_dialog_preserves_ok_caption); harness.run("input dialog rejects empty ok caption", input_dialog_rejects_empty_ok_caption); + harness.run("dialog factory uses pure app-core dialog types", dialog_factory_uses_pure_app_core_dialog_types); return harness.finish(); } diff --git a/tests/app_core/document_export_tests.cpp b/tests/app_core/document_export_tests.cpp index 2fed361..438d84a 100644 --- a/tests/app_core/document_export_tests.cpp +++ b/tests/app_core/document_export_tests.cpp @@ -880,7 +880,7 @@ void export_snapshot_target_support_covers_document_writer_formats(pp::tests::Ha pp::app::DocumentExportExecutionKind::cube_faces)); PP_EXPECT( harness, - !pp::app::document_export_snapshot_target_supported( + pp::app::document_export_snapshot_target_supported( pp::app::DocumentExportExecutionKind::depth)); } @@ -930,7 +930,7 @@ void export_snapshot_route_for_current_platform_uses_platform_policy(pp::tests:: #endif } -void export_snapshot_route_for_current_platform_reports_depth_fallback(pp::tests::Harness& harness) +void export_snapshot_route_for_current_platform_supports_depth_writer(pp::tests::Harness& harness) { pp::app::DocumentCanvasSaveSnapshotReport report; report.payload_complete = true; @@ -940,18 +940,17 @@ void export_snapshot_route_for_current_platform_reports_depth_fallback(pp::tests pp::app::DocumentExportExecutionKind::depth, report); - PP_EXPECT(harness, !plan.uses_document_snapshot_writer); #if __WEB__ + PP_EXPECT(harness, !plan.uses_document_snapshot_writer); PP_EXPECT(harness, !plan.platform_supported); PP_EXPECT( harness, plan.fallback_reason == "document snapshot export is disabled on this platform"); #else + PP_EXPECT(harness, plan.uses_document_snapshot_writer); PP_EXPECT(harness, plan.platform_supported); - PP_EXPECT(harness, !plan.target_supported); - PP_EXPECT( - harness, - plan.fallback_reason == "document snapshot export does not support this target"); + PP_EXPECT(harness, plan.target_supported); + PP_EXPECT(harness, plan.fallback_reason.empty()); #endif } @@ -1382,8 +1381,8 @@ int main() "export snapshot route for current platform uses platform policy", export_snapshot_route_for_current_platform_uses_platform_policy); harness.run( - "export snapshot route for current platform reports depth fallback", - export_snapshot_route_for_current_platform_reports_depth_fallback); + "export snapshot route for current platform supports depth writer", + export_snapshot_route_for_current_platform_supports_depth_writer); harness.run( "export snapshot route falls back for pending renderer payloads", export_snapshot_route_falls_back_for_pending_renderer_payloads); diff --git a/tests/paint_renderer/compositor_tests.cpp b/tests/paint_renderer/compositor_tests.cpp index 592f207..096e1bf 100644 --- a/tests/paint_renderer/compositor_tests.cpp +++ b/tests/paint_renderer/compositor_tests.cpp @@ -13,6 +13,7 @@ using pp::foundation::StatusCode; using pp::paint::BlendMode; using pp::paint::Rgba; using pp::paint::StrokeBlendMode; +using pp::assets::decode_png_rgba8; using pp::paint_renderer::CanvasBlendGateRequest; using pp::paint_renderer::DocumentFaceCompositeRequest; using pp::paint_renderer::DocumentFrameCompositeRequest; @@ -22,6 +23,7 @@ using pp::paint_renderer::StrokeCompositeRequest; using pp::paint_renderer::composite_layer; using pp::paint_renderer::composite_document_face; using pp::paint_renderer::composite_document_frame; +using pp::paint_renderer::export_document_depth_pngs; using pp::paint_renderer::plan_canvas_blend_gate; using pp::paint_renderer::plan_canvas_stroke_feedback; using pp::paint_renderer::plan_document_depth_export_render; @@ -1176,6 +1178,126 @@ void plans_document_depth_export_renderer_work(pp::tests::Harness& h) PP_EXPECT(h, plan.value().requires_renderer_readback); } +void exports_document_depth_as_png_payloads(pp::tests::Harness& h) +{ + const AnimationFrame root_frames[] { + { .duration_ms = 100, .face_pixels = {} }, + }; + const AnimationFrame base_frames[] { + { + .duration_ms = 100, + .face_pixels = { + solid_face_payload(0, 1, 1, 255, 0, 0, 255), + }, + }, + }; + const AnimationFrame top_frames[] { + { + .duration_ms = 100, + .face_pixels = { + solid_face_payload(0, 1, 1, 0, 255, 0, 255), + }, + }, + }; + const DocumentLayerConfig layers[] { + { + .name = "Base", + .frames = std::span(base_frames, 1), + }, + { + .name = "Top", + .frames = std::span(top_frames, 1), + }, + }; + const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { + .width = 1, + .height = 1, + .layers = std::span(layers, 2), + .frames = std::span(root_frames, 1), + .selection_masks = {}, + }); + PP_EXPECT(h, document); + if (!document) { + return; + } + + const auto exported = export_document_depth_pngs( + pp::paint_renderer::DocumentDepthExportRenderPlanRequest { + .document = &document.value(), + .frame_index = 0, + .output_extent = Extent2D { .width = 1, .height = 1 }, + }); + PP_EXPECT(h, exported); + if (!exported) { + return; + } + + PP_EXPECT(h, exported.value().output_extent.width == 1U); + PP_EXPECT(h, exported.value().output_extent.height == 1U); + PP_EXPECT(h, exported.value().image_encoded_bytes > 0U); + PP_EXPECT(h, exported.value().depth_encoded_bytes > 0U); + PP_EXPECT(h, exported.value().merged_face_draw_count == pp::document::cube_face_count); + PP_EXPECT(h, exported.value().layer_depth_draw_count == 2U); + PP_EXPECT(h, exported.value().visited_layer_count == 2U); + PP_EXPECT(h, exported.value().visible_layer_count == 2U); + PP_EXPECT(h, exported.value().face_payload_count == 2U); + PP_EXPECT(h, exported.value().uses_perspective_camera); + + const auto image = decode_png_rgba8(exported.value().image_png); + const auto depth = decode_png_rgba8(exported.value().depth_png); + PP_EXPECT(h, image); + PP_EXPECT(h, depth); + if (!image || !depth) { + return; + } + + PP_EXPECT(h, image.value().width == 1U); + PP_EXPECT(h, image.value().height == 1U); + PP_EXPECT(h, depth.value().width == 1U); + PP_EXPECT(h, depth.value().height == 1U); + const std::vector expected_image { 0, 255, 0, 255 }; + const std::vector expected_depth { 170, 170, 170, 255 }; + PP_EXPECT(h, image.value().pixels == expected_image); + PP_EXPECT(h, depth.value().pixels == expected_depth); +} + +void depth_export_payload_boundary_rejects_malformed_face_bytes(pp::tests::Harness& h) +{ + const AnimationFrame root_frames[] { + { .duration_ms = 100, .face_pixels = {} }, + }; + const AnimationFrame bad_frames[] { + { + .duration_ms = 100, + .face_pixels = { + LayerFacePixels { + .face_index = 0, + .x = 0, + .y = 0, + .width = 1, + .height = 1, + .rgba8 = { 255, 0, 0 }, + }, + }, + }, + }; + const DocumentLayerConfig layers[] { + { + .name = "Broken", + .frames = std::span(bad_frames, 1), + }, + }; + const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { + .width = 1, + .height = 1, + .layers = std::span(layers, 1), + .frames = std::span(root_frames, 1), + .selection_masks = {}, + }); + PP_EXPECT(h, !document.ok()); + PP_EXPECT(h, document.status().code == StatusCode::invalid_argument); +} + void document_frame_upload_rejects_invalid_requests(pp::tests::Harness& h) { RecordingRenderDevice device; @@ -1194,6 +1316,8 @@ void document_frame_upload_rejects_invalid_requests(pp::tests::Harness& h) pp::paint_renderer::DocumentAnimationFrameEquirectangularPngExportRequest {}); const auto no_document_depth = plan_document_depth_export_render( pp::paint_renderer::DocumentDepthExportRenderPlanRequest {}); + const auto no_document_depth_pngs = export_document_depth_pngs( + pp::paint_renderer::DocumentDepthExportRenderPlanRequest {}); const AnimationFrame root_frames[] { { .duration_ms = 100, .face_pixels = {} }, @@ -1232,6 +1356,17 @@ void document_frame_upload_rejects_invalid_requests(pp::tests::Harness& h) .frame_index = 0, .output_extent = Extent2D {}, }); + const auto bad_frame_depth_pngs = export_document_depth_pngs( + pp::paint_renderer::DocumentDepthExportRenderPlanRequest { + .document = &document.value(), + .frame_index = 1, + }); + const auto bad_extent_depth_pngs = export_document_depth_pngs( + pp::paint_renderer::DocumentDepthExportRenderPlanRequest { + .document = &document.value(), + .frame_index = 0, + .output_extent = Extent2D {}, + }); const auto bad_jpeg_quality = pp::paint_renderer::export_document_frame_equirectangular_jpeg( DocumentFrameCompositeRequest { .document = &document.value(), @@ -1253,6 +1388,8 @@ void document_frame_upload_rejects_invalid_requests(pp::tests::Harness& h) PP_EXPECT(h, no_document_frames.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !no_document_depth.ok()); PP_EXPECT(h, no_document_depth.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !no_document_depth_pngs.ok()); + PP_EXPECT(h, no_document_depth_pngs.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_frame.ok()); PP_EXPECT(h, bad_frame.status().code == StatusCode::out_of_range); PP_EXPECT(h, !bad_frame_readiness.ok()); @@ -1261,6 +1398,10 @@ void document_frame_upload_rejects_invalid_requests(pp::tests::Harness& h) PP_EXPECT(h, bad_frame_depth.status().code == StatusCode::out_of_range); PP_EXPECT(h, !bad_extent_depth.ok()); PP_EXPECT(h, bad_extent_depth.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !bad_frame_depth_pngs.ok()); + PP_EXPECT(h, bad_frame_depth_pngs.status().code == StatusCode::out_of_range); + PP_EXPECT(h, !bad_extent_depth_pngs.ok()); + PP_EXPECT(h, bad_extent_depth_pngs.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_jpeg_quality.ok()); PP_EXPECT(h, bad_jpeg_quality.status().code == StatusCode::out_of_range); PP_EXPECT(h, device.commands().empty()); @@ -1623,6 +1764,10 @@ int main() "exports_document_animation_frames_as_equirectangular_pngs", exports_document_animation_frames_as_equirectangular_pngs); harness.run("plans_document_depth_export_renderer_work", plans_document_depth_export_renderer_work); + harness.run("exports_document_depth_as_png_payloads", exports_document_depth_as_png_payloads); + harness.run( + "depth export payload boundary rejects malformed face bytes", + depth_export_payload_boundary_rejects_malformed_face_bytes); harness.run("document_frame_upload_rejects_invalid_requests", document_frame_upload_rejects_invalid_requests); harness.run("detects_feedback_requirements", detects_feedback_requirements); harness.run("plans_stroke_composite_paths", plans_stroke_composite_paths); diff --git a/tests/platform_api/platform_services_tests.cpp b/tests/platform_api/platform_services_tests.cpp index 247cff1..d15abeb 100644 --- a/tests/platform_api/platform_services_tests.cpp +++ b/tests/platform_api/platform_services_tests.cpp @@ -4,6 +4,7 @@ #include "platform_api/network_tls_policy.h" #include "platform_api/platform_policy.h" #include "platform_api/platform_services.h" +#include "platform_apple/apple_platform_services.h" #include #include @@ -495,6 +496,56 @@ private: std::string clipboard_value_; }; +class FakeAppleDocumentPickerBridge final { +public: + [[nodiscard]] pp::platform::apple::AppleDocumentPickerBridge bridge() + { + pp::platform::apple::AppleDocumentPickerBridge result; + result.pick_image = [this](pp::platform::PickedPathCallback callback) { + ++pick_image_requests; + callback(image_path); + }; + result.pick_file = [this]( + std::vector file_types, + pp::platform::PickedPathCallback callback) { + ++pick_file_requests; + picked_file_types = std::move(file_types); + callback(file_path); + }; + result.pick_save_file = [this]( + std::vector file_types, + pp::platform::PickedPathCallback callback) { + ++pick_save_file_requests; + save_file_types = std::move(file_types); + callback(save_path); + }; + result.pick_directory = [this](pp::platform::PickedPathCallback callback) { + ++pick_directory_requests; + callback(directory_path); + }; + result.format_working_directory_path = [this](std::string_view path) { + ++format_requests; + last_format_path.assign(path); + return formatted_path; + }; + return result; + } + + int pick_image_requests = 0; + int pick_file_requests = 0; + int pick_save_file_requests = 0; + int pick_directory_requests = 0; + int format_requests = 0; + std::string image_path = "D:/Paint/import.png"; + std::string file_path = "D:/Paint/demo.ppi"; + std::string save_path = "D:/Paint/export.ppi"; + std::string directory_path = "D:/Paint/work"; + std::string formatted_path = "D:/Paint/Resolved"; + std::string last_format_path; + std::vector picked_file_types; + std::vector save_file_types; +}; + void platform_services_dispatch_clipboard_reads_and_writes(pp::tests::Harness& harness) { FakePlatformServices fake("#112233"); @@ -776,6 +827,117 @@ void platform_services_dispatch_prepared_file_save(pp::tests::Harness& harness) PP_EXPECT(harness, saved); } +void apple_document_platform_services_preserve_browse_root_policy(pp::tests::Harness& harness) +{ + pp::platform::apple::AppleDocumentPlatformServices ios_services(pp::platform::PlatformFamily::ios); + pp::platform::apple::AppleDocumentPlatformServices macos_services(pp::platform::PlatformFamily::macos); + + const auto ios_roots = ios_services.document_browse_roots("D:/Paint/work", "D:/Paint"); + const auto macos_roots = macos_services.document_browse_roots("D:/Paint/work", "D:/Paint"); + + PP_EXPECT(harness, ios_roots.size() == 2); + PP_EXPECT(harness, ios_roots[0] == "D:/Paint/work"); + PP_EXPECT(harness, ios_roots[1] == "D:/Paint/Inbox"); + PP_EXPECT(harness, macos_roots.size() == 1); + PP_EXPECT(harness, macos_roots[0] == "D:/Paint/work"); +} + +void apple_document_platform_services_dispatch_ios_picker_callbacks(pp::tests::Harness& harness) +{ + FakeAppleDocumentPickerBridge fake; + pp::platform::apple::AppleDocumentPlatformServices services( + pp::platform::PlatformFamily::ios, + fake.bridge()); + std::string image_path; + std::string file_path; + std::string save_path = "unchanged"; + std::string directory_path = "unchanged"; + + services.pick_image([&](std::string path) { image_path = std::move(path); }); + services.pick_file({ "ppi", "ppbr" }, [&](std::string path) { file_path = std::move(path); }); + services.pick_save_file({ "ppi" }, [&](std::string path) { save_path = std::move(path); }); + services.pick_directory([&](std::string path) { directory_path = std::move(path); }); + + PP_EXPECT(harness, fake.pick_image_requests == 1); + PP_EXPECT(harness, fake.pick_file_requests == 1); + PP_EXPECT(harness, fake.pick_save_file_requests == 0); + PP_EXPECT(harness, fake.pick_directory_requests == 0); + PP_EXPECT(harness, image_path == "D:/Paint/import.png"); + PP_EXPECT(harness, file_path == "D:/Paint/demo.ppi"); + PP_EXPECT(harness, fake.picked_file_types.size() == 2); + PP_EXPECT(harness, fake.picked_file_types[0] == "ppi"); + PP_EXPECT(harness, fake.picked_file_types[1] == "ppbr"); + PP_EXPECT(harness, save_path == "unchanged"); + PP_EXPECT(harness, directory_path == "unchanged"); +} + +void apple_document_platform_services_filter_macos_picker_paths(pp::tests::Harness& harness) +{ + FakeAppleDocumentPickerBridge fake; + pp::platform::apple::AppleDocumentPlatformServices services( + pp::platform::PlatformFamily::macos, + fake.bridge()); + std::string image_path = "unchanged"; + std::string file_path = "unchanged"; + std::string save_path = "unchanged"; + std::string directory_path = "unchanged"; + + fake.file_path.clear(); + services.pick_image([&](std::string path) { image_path = std::move(path); }); + PP_EXPECT(harness, fake.pick_image_requests == 0); + PP_EXPECT(harness, fake.pick_file_requests == 1); + PP_EXPECT(harness, fake.picked_file_types.size() == 5); + PP_EXPECT(harness, fake.picked_file_types[0] == "png"); + PP_EXPECT(harness, fake.picked_file_types[1] == "PNG"); + PP_EXPECT(harness, fake.picked_file_types[2] == "jpg"); + PP_EXPECT(harness, fake.picked_file_types[3] == "JPG"); + PP_EXPECT(harness, fake.picked_file_types[4] == "jpeg"); + PP_EXPECT(harness, image_path == "unchanged"); + + fake.file_path = "D:/Paint/demo.ppi"; + services.pick_file({ "ppi" }, [&](std::string path) { file_path = std::move(path); }); + PP_EXPECT(harness, fake.pick_file_requests == 2); + PP_EXPECT(harness, file_path == "D:/Paint/demo.ppi"); + + fake.save_path.clear(); + services.pick_save_file({ "ppi" }, [&](std::string path) { save_path = std::move(path); }); + PP_EXPECT(harness, fake.pick_save_file_requests == 1); + PP_EXPECT(harness, save_path == "unchanged"); + + fake.save_path = "D:/Paint/export.ppi"; + services.pick_save_file({ "ppi" }, [&](std::string path) { save_path = std::move(path); }); + PP_EXPECT(harness, fake.pick_save_file_requests == 2); + PP_EXPECT(harness, save_path == "D:/Paint/export.ppi"); + + fake.directory_path.clear(); + services.pick_directory([&](std::string path) { directory_path = std::move(path); }); + PP_EXPECT(harness, fake.pick_directory_requests == 1); + PP_EXPECT(harness, directory_path == "unchanged"); + + fake.directory_path = "D:/Paint/work"; + services.pick_directory([&](std::string path) { directory_path = std::move(path); }); + PP_EXPECT(harness, fake.pick_directory_requests == 2); + PP_EXPECT(harness, directory_path == "D:/Paint/work"); +} + +void apple_document_platform_services_preserve_working_directory_picker_policy(pp::tests::Harness& harness) +{ + FakeAppleDocumentPickerBridge fake; + pp::platform::apple::AppleDocumentPlatformServices ios_services( + pp::platform::PlatformFamily::ios, + fake.bridge()); + pp::platform::apple::AppleDocumentPlatformServices macos_services( + pp::platform::PlatformFamily::macos, + fake.bridge()); + + PP_EXPECT(harness, !ios_services.supports_working_directory_picker()); + PP_EXPECT(harness, macos_services.supports_working_directory_picker()); + PP_EXPECT(harness, ios_services.format_working_directory_path("D:/Paint/.") == "D:/Paint/."); + PP_EXPECT(harness, macos_services.format_working_directory_path("D:/Paint/.") == "D:/Paint/Resolved"); + PP_EXPECT(harness, fake.format_requests == 1); + PP_EXPECT(harness, fake.last_format_path == "D:/Paint/."); +} + void web_platform_services_preserve_default_web_policy(pp::tests::Harness& harness) { ScopedInjectedWebPlatformServices scoped(nullptr); @@ -1209,6 +1371,18 @@ int main() "platform services dispatch working directory picker policy", platform_services_dispatch_working_directory_picker_policy); harness.run("platform services dispatch prepared file save", platform_services_dispatch_prepared_file_save); + harness.run( + "apple document platform services preserve browse root policy", + apple_document_platform_services_preserve_browse_root_policy); + harness.run( + "apple document platform services dispatch ios picker callbacks", + apple_document_platform_services_dispatch_ios_picker_callbacks); + harness.run( + "apple document platform services filter macos picker paths", + apple_document_platform_services_filter_macos_picker_paths); + harness.run( + "apple document platform services preserve working directory picker policy", + apple_document_platform_services_preserve_working_directory_picker_policy); harness.run("web platform services preserve default web policy", web_platform_services_preserve_default_web_policy); harness.run("web platform services resolve injected services", web_platform_services_resolve_injected_services); harness.run("platform services dispatch writable file target", platform_services_dispatch_writable_file_target);