From 08d8c1e82cd39e10fc60c3075f673ad930edf858 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Thu, 4 Jun 2026 18:49:48 +0200 Subject: [PATCH] Move layout reload policy into platform api --- CMakeLists.txt | 4 +++ docs/modernization/build-inventory.md | 9 +++-- docs/modernization/debt.md | 1 + docs/modernization/roadmap.md | 7 ++++ src/layout.cpp | 19 +++++----- src/layout.h | 4 ++- src/platform_api/asset_file_load_policy.cpp | 33 +++++++++++++++++ src/platform_api/asset_file_load_policy.h | 19 ++++++++++ .../platform_api/platform_services_tests.cpp | 36 +++++++++++++++++++ 9 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 src/platform_api/asset_file_load_policy.cpp create mode 100644 src/platform_api/asset_file_load_policy.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 91186ff..191203e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -212,6 +212,8 @@ target_link_libraries(pp_ui_core pp_project_warnings) add_library(pp_platform_api STATIC + src/platform_api/asset_file_load_policy.cpp + src/platform_api/asset_file_load_policy.h src/platform_api/network_tls_policy.cpp src/platform_api/network_tls_policy.h src/platform_api/platform_services.cpp @@ -439,6 +441,7 @@ if(PP_BUILD_APP) pp_project_options PRIVATE pp_paint_renderer + pp_platform_api pp_renderer_api pp_project_warnings) if(TARGET pp_renderer_gl) @@ -475,6 +478,7 @@ if(PP_BUILD_APP) pp_legacy_ui_core pp_project_options PRIVATE + pp_platform_api pp_renderer_api pp_project_warnings) if(TARGET pp_renderer_gl) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index fe4c1d4..f3866bb 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -583,6 +583,11 @@ Known local toolchain state: while non-Windows platforms still reach retained platform bridges through the debt-tracked adapter isolated in `src/platform_legacy/legacy_platform_services.*`. +- `pp_platform_api` also owns `plan_asset_file_load`, the SDK-free file-load + policy consumed by `LayoutManager` for XML layout reload decisions. The + helper preserves desktop mtime-based reload behavior and non-desktop + already-loaded skip behavior while keeping direct platform guards out of the + shared layout parser. - `pp_renderer_gl` owns the tested `OpenGlInitialState` startup depth/blend policy and dispatch application consumed by `App::init`, tested runtime version/vendor/renderer/GLSL string query dispatch, tested default clear @@ -660,8 +665,8 @@ Known local toolchain state: split persistence/runtime dispatch, and malformed startup-plan rejection. - `pp_platform_api_tests` covers service dispatch for clipboard read/write, empty clipboard writes, cursor visibility, virtual-keyboard visibility, - external file display, file sharing, VR lifecycle, and picker callbacks - without platform SDK headers or a window. + external file display, file sharing, VR lifecycle, layout/asset file load + policy, and picker callbacks without platform SDK headers or a window. - `pp_app_core_document_cloud_tests` covers cloud upload no-canvas, new-document warning, clean publish prompt, and dirty save-before-upload decisions, plus cloud browse no-canvas/show-browser and selected-download diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 3dfa780..5c4bca1 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -71,6 +71,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0051 | Open | Modernization | Document browser search roots and Browse dialog working-directory picker visibility/path formatting now dispatch through `PlatformServices`, but iOS `Inbox` inclusion and macOS directory picker/display-path behavior still live 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-0052 | Open | Modernization | Native UI/window state saving now dispatches through `PlatformServices`, but macOS execution still lives in `src/platform_legacy/legacy_platform_services.*` and forwards to the retained Objective-C app bridge | 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 `PlatformServices`, but iOS/Web target selection still lives in `src/platform_legacy/legacy_platform_services.*` | 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`, but that helper still encodes the retained compile-time platform policy: Windows/macOS use `stat` mtime reload checks, while other platforms treat already-loaded layouts as successful no-op loads | 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 | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 4dd6e5c..257642f 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -650,6 +650,9 @@ file display, file sharing, recording file cleanup, live asset/layout reload policy, diagnostic stacktrace/crash hooks, SonarPen availability/startup, VR mode start/stop, image/file/save-file pickers, and directory pickers. +It also owns the SDK-free layout/asset file load policy helper used by +`LayoutManager`, so XML layout hot-reload timestamp checks no longer live in +the shared UI parser. Windows installs an injected `WindowsPlatformServices` implementation from `src/platform_windows/windows_platform_services.*` in `pp_platform_windows`; other platforms still route through the debt-tracked legacy fallback adapter @@ -786,6 +789,9 @@ guards from `App::rec_clear`. The UI loop now asks `PlatformServices` whether live shader/layout reloading should run, preserving the previous Windows/macOS reload behavior while removing the direct `(_WIN32 || __OSX__)` guard from `App::ui_thread_main`. +Layout XML reload read/skip decisions now go through `pp_platform_api` as well, +preserving desktop mtime-based reloads and non-desktop single-load behavior +while removing the direct Windows/macOS guard from `LayoutManager::load`. `App::stacktrace` and `App::crash_test` now dispatch through `PlatformServices`, with Windows retaining the debug-break crash hook and the legacy adapter preserving Apple stacktrace/crash and Android crash-test behavior. @@ -1766,6 +1772,7 @@ Results: collection policy dispatch, network TLS verification policy dispatch, default network TLS policy coverage, PPBR export data-directory policy dispatch, SonarPen availability/startup dispatch, VR lifecycle dispatch, + layout/asset file load policy coverage, live asset/layout reload policy dispatch, diagnostic hook dispatch, per-frame platform hook dispatch, picker callback dispatch, and prepared-file save/download callback dispatch. The live Windows diff --git a/src/layout.cpp b/src/layout.cpp index 8d9bec2..63b5793 100644 --- a/src/layout.cpp +++ b/src/layout.cpp @@ -5,6 +5,7 @@ #include "node.h" #include "node_border.h" #include "layout.h" +#include "platform_api/asset_file_load_policy.h" void LayoutManager::unload() { @@ -22,17 +23,13 @@ void LayoutManager::create() bool LayoutManager::load(const char* path) { auto abs_path = Asset::absolute(path); -#if _WIN32 || __OSX__ - struct stat tmp_info; - if (stat(abs_path.c_str(), &tmp_info) != 0) - return false; - if (tmp_info.st_mtime <= m_file_info.st_mtime) - return false; - m_file_info = tmp_info; -#else - if (m_loaded) - return true; // already loaded -#endif // __ANDROID__ + const auto file_load = pp::platform::plan_asset_file_load( + abs_path, + m_loaded, + m_file_last_write_time); + if (!file_load.should_read_file) + return file_load.skipped_load_result; + m_file_last_write_time = file_load.last_write_time; if (!m_layouts.empty() && on_reloading) on_reloading(); diff --git a/src/layout.h b/src/layout.h index 669416b..632c3bc 100644 --- a/src/layout.h +++ b/src/layout.h @@ -1,9 +1,11 @@ #pragma once +#include + class LayoutManager { std::string m_path; - struct stat m_file_info { 0 }; + std::int64_t m_file_last_write_time = 0; public: std::map> m_layouts; bool m_loaded = false; diff --git a/src/platform_api/asset_file_load_policy.cpp b/src/platform_api/asset_file_load_policy.cpp new file mode 100644 index 0000000..befa059 --- /dev/null +++ b/src/platform_api/asset_file_load_policy.cpp @@ -0,0 +1,33 @@ +#include "platform_api/asset_file_load_policy.h" + +#include +#include + +namespace pp::platform { + +AssetFileLoadDecision plan_asset_file_load( + std::string_view absolute_path, + bool already_loaded, + std::int64_t previous_last_write_time) +{ +#if defined(_WIN32) || defined(__OSX__) + struct stat file_info {}; + const std::string path(absolute_path); + if (stat(path.c_str(), &file_info) != 0) + return { false, false, previous_last_write_time }; + + const auto current_last_write_time = static_cast(file_info.st_mtime); + if (current_last_write_time <= previous_last_write_time) + return { false, false, previous_last_write_time }; + + return { true, true, current_last_write_time }; +#else + if (already_loaded) + return { false, true, previous_last_write_time }; + + (void)absolute_path; + return { true, true, previous_last_write_time }; +#endif +} + +} diff --git a/src/platform_api/asset_file_load_policy.h b/src/platform_api/asset_file_load_policy.h new file mode 100644 index 0000000..0ac793e --- /dev/null +++ b/src/platform_api/asset_file_load_policy.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace pp::platform { + +struct AssetFileLoadDecision { + bool should_read_file = true; + bool skipped_load_result = true; + std::int64_t last_write_time = 0; +}; + +[[nodiscard]] AssetFileLoadDecision plan_asset_file_load( + std::string_view absolute_path, + bool already_loaded, + std::int64_t previous_last_write_time); + +} diff --git a/tests/platform_api/platform_services_tests.cpp b/tests/platform_api/platform_services_tests.cpp index eab19bd..dea43e6 100644 --- a/tests/platform_api/platform_services_tests.cpp +++ b/tests/platform_api/platform_services_tests.cpp @@ -1,8 +1,11 @@ #include "test_harness.h" +#include "platform_api/asset_file_load_policy.h" #include "platform_api/network_tls_policy.h" #include "platform_api/platform_services.h" +#include +#include #include #include #include @@ -757,6 +760,36 @@ void default_network_tls_policy_matches_build_target(pp::tests::Harness& harness #endif } +void asset_file_load_policy_preserves_platform_reload_behavior(pp::tests::Harness& harness) +{ + const auto temp_path = std::filesystem::temp_directory_path() + / "panopainter-platform-api-file-policy.xml"; + { + std::ofstream file(temp_path); + file << ""; + } + + const auto first_load = pp::platform::plan_asset_file_load( + temp_path.string(), + false, + 0); + PP_EXPECT(harness, first_load.should_read_file); + PP_EXPECT(harness, first_load.skipped_load_result); + + const auto repeated_load = pp::platform::plan_asset_file_load( + temp_path.string(), + true, + first_load.last_write_time); + PP_EXPECT(harness, !repeated_load.should_read_file); +#if defined(_WIN32) || defined(__OSX__) + PP_EXPECT(harness, !repeated_load.skipped_load_result); +#else + PP_EXPECT(harness, repeated_load.skipped_load_result); +#endif + + std::filesystem::remove(temp_path); +} + void platform_services_dispatch_ppbr_export_directory_policy(pp::tests::Harness& harness) { FakePlatformServices fake("unused"); @@ -833,6 +866,9 @@ int main() platform_services_dispatch_document_export_collection_policy); harness.run("platform services dispatch network tls policy", platform_services_dispatch_network_tls_policy); harness.run("default network tls policy matches build target", default_network_tls_policy_matches_build_target); + harness.run( + "asset file load policy preserves platform reload behavior", + asset_file_load_policy_preserves_platform_reload_behavior); harness.run( "platform services dispatch ppbr export directory policy", platform_services_dispatch_ppbr_export_directory_policy);