From 32c95b224f45f5c009967cb27b954ddda1e563b3 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Fri, 5 Jun 2026 06:44:57 +0200 Subject: [PATCH] Extend app input planning to UI state --- docs/modernization/build-inventory.md | 3 +- docs/modernization/debt.md | 13 ++++--- docs/modernization/roadmap.md | 17 ++++++--- src/app_core/app_input.h | 43 +++++++++++++++++++++ src/app_events.cpp | 31 ++++++++++++--- tests/CMakeLists.txt | 19 +++++++++ tests/app_core/app_input_tests.cpp | 34 +++++++++++++++++ tools/pano_cli/main.cpp | 55 ++++++++++++++++++++++++++- 8 files changed, 196 insertions(+), 19 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index a16fad5..b9accf9 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -843,7 +843,8 @@ Known local toolchain state: - `pp_app_core_app_input_tests` covers pointer coordinate normalization, invalid pointer/gesture inputs, designer-first mouse routing, mouse-cancel routing, gesture midpoint/distance/delta math, main-layout routing, key state - mutation intent, and VR spacebar camera-sync intent. + mutation intent, VR spacebar camera-sync intent, UI visibility toggle target + selection, malformed UI-toggle layout rejection, and stylus touch-lock intent. - `pp_app_core_app_shutdown_tests` covers legacy shutdown cleanup staging for UI-state save, stroke-preview renderer shutdown, recording stop, texture/shader invalidation, layout unload, render-target/mesh destruction, diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index ba8bdd4..e77b11f 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -100,11 +100,14 @@ agent or engineer to remove them without reconstructing context from chat. render-target recreation, and OpenGL/UI drawing remain in the legacy app. - 2026-06-05: DEBT-0003 was narrowed. Pointer coordinate normalization, mouse designer-first routing, gesture midpoint/delta math, touch/key - main-layout routing, and VR spacebar camera-sync intent are now tested - `pp_app_core` input plans consumed by `App::mouse_*`, `App::gesture_*`, - `App::touch_tap`, `App::key_*`, and `pano_cli plan-app-input`; retained - `MouseEvent`/`GestureEvent`/`TouchEvent`/`KeyEvent` construction and legacy - `Node` event dispatch remain in the app shell. + main-layout routing, VR spacebar camera-sync intent, UI visibility toggling, + and stylus touch-lock attachment are now tested `pp_app_core` input plans + consumed by `App::mouse_*`, `App::gesture_*`, `App::touch_tap`, + `App::key_*`, `App::toggle_ui`, `App::set_stylus`, and + `pano_cli plan-app-input`; retained + `MouseEvent`/`GestureEvent`/`TouchEvent`/`KeyEvent` construction, + UI child-node mutation, and legacy `Node` event dispatch remain in the app + shell. - 2026-06-05: DEBT-0003 was narrowed again. Shutdown cleanup staging for UI-state save, stroke-preview renderer shutdown, recording stop, texture/shader invalidation, layout unload, UI render-target and face-plane diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index e892496..0d46462 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -206,10 +206,12 @@ VR UI drawing, main UI drawing, and redraw reset now live in `pp_app_core`; render-target recreation, and OpenGL/UI drawing stay in the legacy app. App input dispatch decisions for pointer coordinate normalization, mouse designer-first routing, gesture midpoint/delta math, touch/key main-layout -routing, and VR spacebar camera-sync intent now live in `pp_app_core`; -`App::mouse_*`, `App::gesture_*`, `App::touch_tap`, `App::key_*`, and -`pano_cli plan-app-input` consume those plans while retained event objects and -legacy `Node` dispatch stay in the app shell. +routing, VR spacebar camera-sync intent, UI visibility toggling, and stylus +touch-lock attachment now live in `pp_app_core`; `App::mouse_*`, +`App::gesture_*`, `App::touch_tap`, `App::key_*`, `App::toggle_ui`, +`App::set_stylus`, and `pano_cli plan-app-input` consume those plans while +retained event objects, child-node mutation, and legacy `Node` dispatch stay +in the app shell. Shutdown lifecycle staging for UI-state save, stroke-preview renderer shutdown, recording stop, texture/shader invalidation, layout unload, render-target destruction, panel-node release, and quick-mode cleanup now lives in @@ -1678,8 +1680,11 @@ Results: - Focused app-input CTest coverage passed for `pp_app_core_app_input_tests`, `pano_cli_plan_app_input_pointer_smoke`, `pano_cli_plan_app_input_gesture_smoke`, - `pano_cli_plan_app_input_key_vr_smoke`, and - `pano_cli_plan_app_input_rejects_bad_float`. + `pano_cli_plan_app_input_key_vr_smoke`, + `pano_cli_plan_app_input_ui_toggle_smoke`, + `pano_cli_plan_app_input_stylus_smoke`, + `pano_cli_plan_app_input_rejects_bad_float`, and + `pano_cli_plan_app_input_rejects_missing_ui_panel`. - `PanoPainter`, `pp_app_core_app_shutdown_tests`, and `pano_cli` built after shutdown cleanup staging moved into `pp_app_core`. - Focused shutdown CTest coverage passed for `pp_app_core_app_shutdown_tests`, diff --git a/src/app_core/app_input.h b/src/app_core/app_input.h index 9a740a5..4d37b8b 100644 --- a/src/app_core/app_input.h +++ b/src/app_core/app_input.h @@ -3,6 +3,7 @@ #include "foundation/result.h" #include +#include namespace pp::app { @@ -37,6 +38,17 @@ struct AppKeyDispatchPlan { bool sync_vr_camera_rotation = false; }; +struct AppUiVisibilityTogglePlan { + bool next_ui_visible = true; + std::size_t first_panel_child_index = 1; + std::size_t panel_child_count = 0; +}; + +struct AppStylusAttachPlan { + bool set_has_stylus = true; + bool enable_canvas_touch_lock = false; +}; + [[nodiscard]] inline pp::foundation::Status validate_input_zoom(float zoom) { if (!std::isfinite(zoom) || zoom <= 0.0F) { @@ -163,4 +175,35 @@ struct AppKeyDispatchPlan { }; } +[[nodiscard]] inline pp::foundation::Result plan_app_ui_visibility_toggle( + bool current_ui_visible, + bool has_main_layout, + std::size_t main_child_count, + std::size_t panel_child_count) +{ + if (!has_main_layout) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("UI toggle requires a main layout")); + } + + if (main_child_count <= 1U) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("UI toggle requires a panel container child")); + } + + return pp::foundation::Result::success(AppUiVisibilityTogglePlan { + .next_ui_visible = !current_ui_visible, + .first_panel_child_index = 1U, + .panel_child_count = panel_child_count, + }); +} + +[[nodiscard]] constexpr AppStylusAttachPlan plan_app_stylus_attach(bool has_canvas) noexcept +{ + return AppStylusAttachPlan { + .set_has_stylus = true, + .enable_canvas_touch_lock = has_canvas, + }; +} + } // namespace pp::app diff --git a/src/app_events.cpp b/src/app_events.cpp index 1c5871c..dbf7945 100644 --- a/src/app_events.cpp +++ b/src/app_events.cpp @@ -661,15 +661,34 @@ bool App::key_char(char key) void App::toggle_ui() { - auto m = layout[main_id]->m_children[1]; - ui_visible = !ui_visible; - for (int i = 1; i < m->m_children.size(); i++) - m->m_children[i]->m_display = ui_visible; + auto* main = layout[main_id]; + const std::size_t main_child_count = main ? main->m_children.size() : 0U; + auto* panel_container = main_child_count > 1U ? main->m_children[1].get() : nullptr; + const auto plan = pp::app::plan_app_ui_visibility_toggle( + ui_visible, + main != nullptr, + main_child_count, + panel_container ? panel_container->m_children.size() : 0U); + if (!plan) { + LOG("UI toggle plan failed: %s", plan.status().message); + return; + } + + ui_visible = plan.value().next_ui_visible; + if (!panel_container) + return; + + for (std::size_t i = plan.value().first_panel_child_index; + i < plan.value().panel_child_count; + ++i) { + panel_container->m_children[i]->m_display = ui_visible; + } } void App::set_stylus() { - has_stylus = true; - if (canvas) + const auto plan = pp::app::plan_app_stylus_attach(canvas != nullptr); + has_stylus = plan.set_has_stylus; + if (plan.enable_canvas_touch_lock && canvas) canvas->m_canvas->m_touch_lock = true; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9f22246..867bc62 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1030,6 +1030,18 @@ if(TARGET pano_cli) LABELS "app;integration;desktop-fast" PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-input\".*\"kind\":\"key\".*\"dispatchMain\":true.*\"setKeyDown\":true.*\"syncVrCameraRotation\":true") + add_test(NAME pano_cli_plan_app_input_ui_toggle_smoke + COMMAND pano_cli plan-app-input --kind ui-toggle --panel-child-count 4) + set_tests_properties(pano_cli_plan_app_input_ui_toggle_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-input\".*\"kind\":\"ui-toggle\".*\"nextUiVisible\":false.*\"firstPanelChildIndex\":1.*\"panelChildCount\":4") + + add_test(NAME pano_cli_plan_app_input_stylus_smoke + COMMAND pano_cli plan-app-input --kind stylus --no-canvas) + set_tests_properties(pano_cli_plan_app_input_stylus_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-input\".*\"kind\":\"stylus\".*\"setHasStylus\":true.*\"enableCanvasTouchLock\":false") + add_test(NAME pano_cli_plan_app_input_rejects_bad_float COMMAND pano_cli plan-app-input --kind pointer --bad-float) set_tests_properties(pano_cli_plan_app_input_rejects_bad_float PROPERTIES @@ -1037,6 +1049,13 @@ if(TARGET pano_cli) WILL_FAIL TRUE PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-input\".*\"message\":\"input zoom must be finite and positive\"") + add_test(NAME pano_cli_plan_app_input_rejects_missing_ui_panel + COMMAND pano_cli plan-app-input --kind ui-toggle --main-child-count 1) + set_tests_properties(pano_cli_plan_app_input_rejects_missing_ui_panel PROPERTIES + LABELS "app;ui;integration;desktop-fast;fuzz" + WILL_FAIL TRUE + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-input\".*\"message\":\"UI toggle requires a panel container child\"") + add_test(NAME pano_cli_plan_app_shutdown_smoke COMMAND pano_cli plan-app-shutdown) set_tests_properties(pano_cli_plan_app_shutdown_smoke PROPERTIES diff --git a/tests/app_core/app_input_tests.cpp b/tests/app_core/app_input_tests.cpp index 41b1e9b..11da0d5 100644 --- a/tests/app_core/app_input_tests.cpp +++ b/tests/app_core/app_input_tests.cpp @@ -102,6 +102,37 @@ void simple_input_plan_tracks_main_layout_availability(pp::tests::Harness& harne PP_EXPECT(harness, !missing.dispatch_main); } +void ui_visibility_toggle_plan_flips_state_and_targets_panel_children(pp::tests::Harness& harness) +{ + const auto hidden = pp::app::plan_app_ui_visibility_toggle(true, true, 2U, 4U); + const auto visible = pp::app::plan_app_ui_visibility_toggle(false, true, 3U, 1U); + + PP_EXPECT(harness, hidden); + PP_EXPECT(harness, !hidden.value().next_ui_visible); + PP_EXPECT(harness, hidden.value().first_panel_child_index == 1U); + PP_EXPECT(harness, hidden.value().panel_child_count == 4U); + PP_EXPECT(harness, visible); + PP_EXPECT(harness, visible.value().next_ui_visible); + PP_EXPECT(harness, visible.value().panel_child_count == 1U); +} + +void ui_visibility_toggle_plan_rejects_missing_panel_container(pp::tests::Harness& harness) +{ + PP_EXPECT(harness, !pp::app::plan_app_ui_visibility_toggle(true, false, 0U, 0U)); + PP_EXPECT(harness, !pp::app::plan_app_ui_visibility_toggle(true, true, 1U, 0U)); +} + +void stylus_attach_plan_sets_touch_lock_only_when_canvas_exists(pp::tests::Harness& harness) +{ + const auto with_canvas = pp::app::plan_app_stylus_attach(true); + const auto without_canvas = pp::app::plan_app_stylus_attach(false); + + PP_EXPECT(harness, with_canvas.set_has_stylus); + PP_EXPECT(harness, with_canvas.enable_canvas_touch_lock); + PP_EXPECT(harness, without_canvas.set_has_stylus); + PP_EXPECT(harness, !without_canvas.enable_canvas_touch_lock); +} + } // namespace int main() @@ -114,5 +145,8 @@ int main() harness.run("gesture plan rejects invalid input", gesture_plan_rejects_invalid_input); harness.run("key plan tracks state and VR spacebar sync", key_plan_tracks_state_and_vr_spacebar_sync); harness.run("simple input plan tracks main layout availability", simple_input_plan_tracks_main_layout_availability); + harness.run("UI visibility toggle plan flips state and targets panel children", ui_visibility_toggle_plan_flips_state_and_targets_panel_children); + harness.run("UI visibility toggle plan rejects missing panel container", ui_visibility_toggle_plan_rejects_missing_panel_container); + harness.run("stylus attach plan sets touch lock only when canvas exists", stylus_attach_plan_sets_touch_lock_only_when_canvas_exists); return harness.finish(); } diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 988f96c..1c1f0f5 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -280,6 +280,10 @@ struct PlanAppInputArgs { bool spacebar = false; bool vr_active = false; bool key_up = false; + bool ui_visible = true; + bool has_canvas = true; + std::uint32_t main_child_count = 2; + std::uint32_t panel_child_count = 4; bool bad_float = false; }; @@ -2071,7 +2075,7 @@ void print_help() << " plan-app-startup [--run-counter N] [--auto-timelapse-disabled] [--vr-controllers-disabled] [--license-invalid]\n" << " plan-app-startup-resources [--width N] [--height N] [--bad-size]\n" << " plan-app-frame [--redraw] [--animate] [--no-designer-layout] [--no-main-layout] [--no-canvas] [--no-canvas-document] [--vr-active] [--ui-hidden] [--vr-only] [--resize-width N] [--resize-height N] [--bad-resize]\n" - << " plan-app-input --kind pointer|gesture|cancel|main|key [--x N] [--y N] [--x1 N] [--y1 N] [--prev-x N] [--prev-y N] [--prev-x1 N] [--prev-y1 N] [--zoom N] [--no-designer-layout] [--no-main-layout] [--spacebar] [--vr-active] [--key-up] [--bad-float]\n" + << " plan-app-input --kind pointer|gesture|cancel|main|key|ui-toggle|stylus [--x N] [--y N] [--x1 N] [--y1 N] [--prev-x N] [--prev-y N] [--prev-x1 N] [--prev-y1 N] [--zoom N] [--no-designer-layout] [--no-main-layout] [--spacebar] [--vr-active] [--key-up] [--ui-hidden] [--no-canvas] [--main-child-count N] [--panel-child-count N] [--bad-float]\n" << " plan-app-shutdown\n" << " plan-command-convert [--project FILE] [--output FILE] [--canvas-resolution N]\n" << " plan-app-status [--doc-name NAME] [--unsaved] [--resolution N] [--resolution-index N] [--zoom N] [--history-bytes N] [--recording-running] [--encoder-available] [--encoded-frames N] [--framebuffer-fetch] [--float32] [--float32-linear] [--float16]\n" @@ -3960,6 +3964,28 @@ pp::foundation::Status parse_plan_app_input_args( args.vr_active = true; } else if (key == "--key-up") { args.key_up = true; + } else if (key == "--ui-hidden") { + args.ui_visible = false; + } else if (key == "--no-canvas") { + args.has_canvas = false; + } else if (key == "--main-child-count") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = pp::foundation::parse_u32(argv[++i]); + if (!value) { + return value.status(); + } + args.main_child_count = value.value(); + } else if (key == "--panel-child-count") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = pp::foundation::parse_u32(argv[++i]); + if (!value) { + return value.status(); + } + args.panel_child_count = value.value(); } else if (key == "--bad-float") { args.bad_float = true; } else { @@ -4070,6 +4096,33 @@ int plan_app_input(int argc, char** argv) return 0; } + if (args.kind == "ui-toggle") { + const auto plan = pp::app::plan_app_ui_visibility_toggle( + args.ui_visible, + args.has_main_layout, + args.main_child_count, + args.panel_child_count); + if (!plan) { + print_error("plan-app-input", plan.status().message); + return 2; + } + std::cout << "{\"ok\":true,\"command\":\"plan-app-input\",\"kind\":\"ui-toggle\"" + << ",\"nextUiVisible\":" << json_bool(plan.value().next_ui_visible) + << ",\"firstPanelChildIndex\":" << plan.value().first_panel_child_index + << ",\"panelChildCount\":" << plan.value().panel_child_count + << "}\n"; + return 0; + } + + if (args.kind == "stylus") { + const auto plan = pp::app::plan_app_stylus_attach(args.has_canvas); + std::cout << "{\"ok\":true,\"command\":\"plan-app-input\",\"kind\":\"stylus\"" + << ",\"setHasStylus\":" << json_bool(plan.set_has_stylus) + << ",\"enableCanvasTouchLock\":" << json_bool(plan.enable_canvas_touch_lock) + << "}\n"; + return 0; + } + print_error("plan-app-input", "unknown input plan kind"); return 2; }