From 5ee2dd271c8161554176c7baabdabb06dcbe9ea7 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Wed, 3 Jun 2026 03:47:28 +0200 Subject: [PATCH] Plan cursor visibility in app core --- docs/modernization/build-inventory.md | 6 +- docs/modernization/capability-map.md | 2 +- docs/modernization/debt.md | 1 + docs/modernization/roadmap.md | 11 +++- src/app_core/document_platform_io.h | 12 ++++ src/app_events.cpp | 14 +++++ tests/CMakeLists.txt | 12 ++++ tests/app_core/document_platform_io_tests.cpp | 16 ++++++ tools/pano_cli/main.cpp | 55 +++++++++++++++++++ 9 files changed, 125 insertions(+), 4 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 0efe60d..ae664cb 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -437,6 +437,9 @@ Known local toolchain state: visibility planning as JSON for hidden and visible states; live show/hide keyboard requests consume the same contract before retained mobile platform keyboard bridges. +- `pano_cli plan-cursor-visibility` exposes `pp_app_core` cursor visibility + planning as JSON for hidden and visible states; live canvas cursor requests + consume the same contract before retained desktop platform cursor bridges. - `pano_cli plan-cloud-upload` exposes `pp_app_core` cloud upload availability, new-document warning, publish prompt, and save-before-upload planning as JSON; the live cloud upload command consumes the same start contract before @@ -470,7 +473,8 @@ Known local toolchain state: - `pp_app_core_document_platform_io_tests` covers empty selected-path filtering and non-empty picked-path callback planning, plus empty/non-empty display-file planning before platform picker/display callbacks, plus virtual keyboard - show/hide planning before platform keyboard callbacks. + show/hide planning before platform keyboard callbacks, plus cursor visibility + planning before platform cursor callbacks. - `pp_app_core_document_cloud_tests` covers cloud upload no-canvas, new-document warning, clean publish prompt, and dirty save-before-upload decisions, plus cloud browse no-canvas/show-browser and selected-download diff --git a/docs/modernization/capability-map.md b/docs/modernization/capability-map.md index df547ca..76f9253 100644 --- a/docs/modernization/capability-map.md +++ b/docs/modernization/capability-map.md @@ -65,7 +65,7 @@ and validation command. | Capability | Current Area | Target Owner | Required Tests | | --- | --- | --- | --- | -| Mouse/keyboard/touch/gestures | `App`, platform entrypoints | `pp_platform_*`, app | Synthetic event playback | +| Mouse/keyboard/touch/gestures/cursor | `App`, platform entrypoints | `pp_app_core`, `pp_platform_*`, app | Cursor visibility decision tests, synthetic event playback | | Wacom pressure | `WacomTablet` | `pp_platform_windows` | Adapter smoke with fallback | | Clipboard/file picker/share/display | `App` platform methods | `pp_app_core`, `pp_platform_*` | Share saved-path, picked-path, and display-file decision tests, platform smoke or mocked service | | Virtual keyboard | `App`, platform entrypoints | `pp_app_core`, `pp_platform_*` | Keyboard visibility decision tests, platform smoke | diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 9886e9b..e09f253 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -33,6 +33,7 @@ agent or engineer to remove them without reconstructing context from chat. | 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, and decoded pixel attachment to `pp_document`, but full legacy PPI round-trip parity is 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 | | DEBT-0014 | Open | Modernization | `windows-clangcl-asan` now configures as a headless Ninja/clang-cl preset and uses the release MSVC runtime required by ASan, but local builds still fail because installed clang-cl 18.1.8 is paired with VS 2026-preview STL headers that require Clang 20 or newer | Sanitizer validation should be local and repeatable, but this machine's compiler/header pairing is incompatible | `cmake --fresh --preset windows-clangcl-asan`; `cmake --build --preset windows-clangcl-asan --target pp_foundation` | Install/use Clang 20+ with the VS 2026 STL, or point the preset at a compatible VS 2022 toolchain, then make `platform-build.ps1 -Presets windows-clangcl-asan` pass for the headless matrix | +| DEBT-0015 | Open | Modernization | Cursor visibility requests now consume pure `pp_app_core` planning through `pano_cli plan-cursor-visibility`, but live cursor execution still reaches retained Win32/macOS platform bridges from `App::show_cursor` and `App::hide_cursor` | Keep canvas cursor behavior stable while platform shells are extracted incrementally | `pp_app_core_document_platform_io_tests`; `pano_cli plan-cursor-visibility --visible`; `ctest --preset desktop-fast --build-config Debug` | Cursor visibility execution is owned by `pp_platform_*` services and live app code depends on an injected platform interface instead of direct singleton/platform calls | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 2a173ee..4220f69 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -460,6 +460,9 @@ bridges continue. `pano_cli plan-keyboard-visibility` exposes the app-core virtual keyboard visibility decision used by live show/hide keyboard requests before retained mobile platform keyboard bridges continue. +`pano_cli plan-cursor-visibility` exposes the app-core cursor visibility +decision used by live canvas cursor requests before retained desktop platform +cursor bridges continue. `pano_cli plan-cloud-upload` exposes the app-core cloud upload decision used by the live cloud upload command for missing-canvas, new-document warning, publish prompt, and dirty-document save-before-upload states before legacy UI, canvas, @@ -950,7 +953,8 @@ Results: filtering and non-empty picked-path callback planning before platform picker callbacks, plus empty/non-empty display-file planning before platform display callbacks, plus virtual keyboard show/hide planning before platform - keyboard callbacks. + keyboard callbacks, plus cursor visibility planning before platform cursor + callbacks. - `pano_cli_plan_picked_path_empty_smoke` and `pano_cli_plan_picked_path_selected_smoke` passed and expose app-core picker selected-path decisions as JSON. @@ -960,6 +964,9 @@ Results: - `pano_cli_plan_keyboard_visibility_hidden_smoke` and `pano_cli_plan_keyboard_visibility_visible_smoke` passed and expose app-core virtual keyboard decisions as JSON. +- `pano_cli_plan_cursor_visibility_hidden_smoke` and + `pano_cli_plan_cursor_visibility_visible_smoke` passed and expose app-core + cursor visibility decisions as JSON. - `panopainter_validate_shaders` passed, validating 25 shader programs and 7 shader includes for stage markers and include graph integrity. - `pp_renderer_gl_capabilities_tests` passed on default MSVC, vcpkg-headless, @@ -1150,7 +1157,7 @@ Use this as the starting checklist for Phase 0 inventory. - Input: mouse, keyboard, touch, gestures, Wacom tablet, stylus pressure, VR controllers. - Platform services: clipboard, file picker, save picker, directory picker, - share/display file, keyboard show/hide. + share/display file, keyboard show/hide, cursor visibility. - VR/platform variants: OpenVR desktop, Quest, Focus/Wave, Android standard, iOS/macOS, Linux, WebGL. - Cloud/network: upload, download, browse, license/check flows. diff --git a/src/app_core/document_platform_io.h b/src/app_core/document_platform_io.h index e62c8a2..c5e616f 100644 --- a/src/app_core/document_platform_io.h +++ b/src/app_core/document_platform_io.h @@ -19,6 +19,11 @@ enum class VirtualKeyboardAction { hide_keyboard, }; +enum class CursorVisibilityAction { + show_cursor, + hide_cursor, +}; + [[nodiscard]] constexpr PickedPathAction plan_picked_path(std::string_view path) noexcept { return path.empty() @@ -40,4 +45,11 @@ enum class VirtualKeyboardAction { : VirtualKeyboardAction::hide_keyboard; } +[[nodiscard]] constexpr CursorVisibilityAction plan_cursor_visibility(bool visible) noexcept +{ + return visible + ? CursorVisibilityAction::show_cursor + : CursorVisibilityAction::hide_cursor; +} + } diff --git a/src/app_events.cpp b/src/app_events.cpp index cd2e571..6edca1e 100644 --- a/src/app_events.cpp +++ b/src/app_events.cpp @@ -27,6 +27,14 @@ void invoke_picked_path_if_selected( : action == pp::app::VirtualKeyboardAction::hide_keyboard; } +[[nodiscard]] bool should_dispatch_cursor_visibility(bool visible) noexcept +{ + const auto action = pp::app::plan_cursor_visibility(visible); + return visible + ? action == pp::app::CursorVisibilityAction::show_cursor + : action == pp::app::CursorVisibilityAction::hide_cursor; +} + } #ifdef __ANDROID__ @@ -120,6 +128,9 @@ void App::resize(float w, float h) void App::show_cursor() { + if (!should_dispatch_cursor_visibility(true)) + return; + #ifdef _WIN32 win32_show_cursor(true); #elif __OSX__ @@ -129,6 +140,9 @@ void App::show_cursor() void App::hide_cursor() { + if (!should_dispatch_cursor_visibility(false)) + return; + #ifdef _WIN32 win32_show_cursor(false); #elif __OSX__ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d5a7122..a689914 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -639,6 +639,18 @@ if(TARGET pano_cli) LABELS "app;integration;desktop-fast" PASS_REGULAR_EXPRESSION "\"command\":\"plan-keyboard-visibility\".*\"visible\":true.*\"decision\":\"show-keyboard\"") + add_test(NAME pano_cli_plan_cursor_visibility_hidden_smoke + COMMAND pano_cli plan-cursor-visibility) + set_tests_properties(pano_cli_plan_cursor_visibility_hidden_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-cursor-visibility\".*\"visible\":false.*\"decision\":\"hide-cursor\"") + + add_test(NAME pano_cli_plan_cursor_visibility_visible_smoke + COMMAND pano_cli plan-cursor-visibility --visible) + set_tests_properties(pano_cli_plan_cursor_visibility_visible_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-cursor-visibility\".*\"visible\":true.*\"decision\":\"show-cursor\"") + add_test(NAME pano_cli_simulate_app_session_clean_smoke COMMAND pano_cli simulate-app-session) set_tests_properties(pano_cli_simulate_app_session_clean_smoke PROPERTIES diff --git a/tests/app_core/document_platform_io_tests.cpp b/tests/app_core/document_platform_io_tests.cpp index 618d9c5..779f7fb 100644 --- a/tests/app_core/document_platform_io_tests.cpp +++ b/tests/app_core/document_platform_io_tests.cpp @@ -45,6 +45,20 @@ void virtual_keyboard_plans_hide_action(pp::tests::Harness& harness) pp::app::plan_virtual_keyboard(false) == pp::app::VirtualKeyboardAction::hide_keyboard); } +void cursor_visibility_plans_show_action(pp::tests::Harness& harness) +{ + PP_EXPECT( + harness, + pp::app::plan_cursor_visibility(true) == pp::app::CursorVisibilityAction::show_cursor); +} + +void cursor_visibility_plans_hide_action(pp::tests::Harness& harness) +{ + PP_EXPECT( + harness, + pp::app::plan_cursor_visibility(false) == pp::app::CursorVisibilityAction::hide_cursor); +} + } int main() @@ -56,5 +70,7 @@ int main() harness.run("display file opens nonempty path", display_file_opens_nonempty_path); harness.run("virtual keyboard plans show action", virtual_keyboard_plans_show_action); harness.run("virtual keyboard plans hide action", virtual_keyboard_plans_hide_action); + harness.run("cursor visibility plans show action", cursor_visibility_plans_show_action); + harness.run("cursor visibility plans hide action", cursor_visibility_plans_hide_action); return harness.finish(); } diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index c98c06a..cb01f0e 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -178,6 +178,10 @@ struct PlanKeyboardVisibilityArgs { bool visible = false; }; +struct PlanCursorVisibilityArgs { + bool visible = false; +}; + struct SimulateAppSessionArgs { bool has_canvas = true; bool new_document = false; @@ -547,6 +551,18 @@ const char* virtual_keyboard_action_name(pp::app::VirtualKeyboardAction action) return "hide-keyboard"; } +const char* cursor_visibility_action_name(pp::app::CursorVisibilityAction action) noexcept +{ + switch (action) { + case pp::app::CursorVisibilityAction::show_cursor: + return "show-cursor"; + case pp::app::CursorVisibilityAction::hide_cursor: + return "hide-cursor"; + } + + return "hide-cursor"; +} + pp::foundation::Result parse_float_arg(std::string_view text) { float value = 0.0F; @@ -591,6 +607,7 @@ void print_help() << " plan-picked-path [--path FILE]\n" << " plan-display-file [--path FILE]\n" << " plan-keyboard-visibility [--visible]\n" + << " plan-cursor-visibility [--visible]\n" << " load-project --path FILE\n" << " parse-layout --path FILE\n" << " record-render [--width N] [--height N] [--exercise-clear]\n" @@ -2069,6 +2086,40 @@ int plan_keyboard_visibility(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_cursor_visibility_args( + int argc, + char** argv, + PlanCursorVisibilityArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--visible") { + args.visible = true; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_cursor_visibility(int argc, char** argv) +{ + PlanCursorVisibilityArgs args; + const auto status = parse_plan_cursor_visibility_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-cursor-visibility", status.message); + return 2; + } + + const auto decision = pp::app::plan_cursor_visibility(args.visible); + std::cout << "{\"ok\":true,\"command\":\"plan-cursor-visibility\"" + << ",\"state\":{\"visible\":" << json_bool(args.visible) + << "},\"decision\":\"" << cursor_visibility_action_name(decision) + << "\"}\n"; + return 0; +} + pp::foundation::Status parse_plan_export_target_args( int argc, char** argv, @@ -4252,6 +4303,10 @@ int main(int argc, char** argv) return plan_keyboard_visibility(argc, argv); } + if (command == "plan-cursor-visibility") { + return plan_cursor_visibility(argc, argv); + } + if (command == "load-project") { return load_project(argc, argv); }