From b825d920d257185524c76836d9f6aaff656da727 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Fri, 5 Jun 2026 06:38:48 +0200 Subject: [PATCH] Route app input dispatch through app core --- CMakeLists.txt | 1 + docs/modernization/build-inventory.md | 4 + docs/modernization/debt.md | 7 + docs/modernization/roadmap.md | 13 ++ scripts/automation/platform-build.ps1 | 1 + scripts/automation/platform-build.sh | 2 +- src/app_core/app_input.h | 166 ++++++++++++++++++++ src/app_events.cpp | 176 +++++++++++++++------ tests/CMakeLists.txt | 35 +++++ tests/app_core/app_input_tests.cpp | 118 ++++++++++++++ tools/pano_cli/main.cpp | 218 ++++++++++++++++++++++++++ 11 files changed, 695 insertions(+), 46 deletions(-) create mode 100644 src/app_core/app_input.h create mode 100644 tests/app_core/app_input_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5ef003a..ae3de40 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -241,6 +241,7 @@ target_link_libraries(pp_platform_api add_library(pp_app_core STATIC src/app_core/about_menu.h src/app_core/app_frame.h + src/app_core/app_input.h src/app_core/app_preferences.h src/app_core/app_shutdown.h src/app_core/app_status.h diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index e00c62e..a16fad5 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -840,6 +840,10 @@ Known local toolchain state: visibility, main UI suppression in VR-only mode, tick layout selection, resize render-target/redraw projection, invalid resize rejection, and redraw reset planning. +- `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. - `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 92ed478..ba8bdd4 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -98,6 +98,13 @@ agent or engineer to remove them without reconstructing context from chat. `App::create`, `App::tick`, `App::resize`, `App::update`, `App::draw`, and `pano_cli plan-app-frame`; retained layout traversal, toolbar widget writes, 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. - 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 78e146f..e892496 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -204,6 +204,12 @@ VR UI drawing, main UI drawing, and redraw reset now live in `pp_app_core`; `App::create`, `App::tick`, `App::resize`, `App::update`, `App::draw`, and `pano_cli plan-app-frame` consume those plans while retained layout traversal, 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. 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 @@ -1667,6 +1673,13 @@ Results: `pano_cli_plan_app_frame_idle_missing_canvas_smoke`, with resize automation covered by `pano_cli_plan_app_frame_resize_smoke` and `pano_cli_plan_app_frame_rejects_bad_resize`. +- `PanoPainter`, `pp_app_core_app_input_tests`, and `pano_cli` built after + app input routing and normalization moved into `pp_app_core`. +- 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`. - `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/scripts/automation/platform-build.ps1 b/scripts/automation/platform-build.ps1 index 5cf445d..2bdce8b 100644 --- a/scripts/automation/platform-build.ps1 +++ b/scripts/automation/platform-build.ps1 @@ -43,6 +43,7 @@ param( "pp_app_core_about_menu_tests", "pp_app_core_app_preferences_tests", "pp_app_core_app_frame_tests", + "pp_app_core_app_input_tests", "pp_app_core_app_shutdown_tests", "pp_app_core_app_startup_tests", "pp_app_core_app_status_tests", diff --git a/scripts/automation/platform-build.sh b/scripts/automation/platform-build.sh index d353beb..02934c9 100644 --- a/scripts/automation/platform-build.sh +++ b/scripts/automation/platform-build.sh @@ -3,7 +3,7 @@ set -u presets="${1:-android-arm64 android-x64 android-quest-arm64 android-focus-arm64}" shift || true -targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_renderer_gl pp_paint_renderer pp_ui_core pp_platform_api pp_app_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_brush_package_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_tests pp_document_tests pp_document_ppi_import_tests pp_document_ppi_export_tests pp_renderer_api_tests pp_renderer_gl_capabilities_tests pp_renderer_gl_command_plan_tests pp_paint_renderer_compositor_tests pp_platform_api_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pp_app_core_about_menu_tests pp_app_core_app_preferences_tests pp_app_core_app_frame_tests pp_app_core_app_shutdown_tests pp_app_core_app_startup_tests pp_app_core_app_status_tests pp_app_core_command_convert_tests pp_app_core_brush_package_export_tests pp_app_core_brush_package_import_tests pp_app_core_brush_ui_tests pp_app_core_canvas_hotkey_tests pp_app_core_canvas_tool_ui_tests pp_app_core_canvas_view_tests pp_app_core_document_animation_tests pp_app_core_document_canvas_tests pp_app_core_document_cloud_tests pp_app_core_document_export_tests pp_app_core_document_import_tests pp_app_core_document_layer_tests pp_app_core_document_platform_io_tests pp_app_core_document_recording_tests pp_app_core_document_resize_tests pp_app_core_document_route_tests pp_app_core_document_sharing_tests pp_app_core_document_session_tests pp_app_core_file_menu_tests pp_app_core_grid_ui_tests pp_app_core_history_ui_tests pp_app_core_main_toolbar_tests pp_app_core_quick_ui_tests pp_app_core_tools_menu_tests}" +targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_renderer_gl pp_paint_renderer pp_ui_core pp_platform_api pp_app_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_brush_package_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_tests pp_document_tests pp_document_ppi_import_tests pp_document_ppi_export_tests pp_renderer_api_tests pp_renderer_gl_capabilities_tests pp_renderer_gl_command_plan_tests pp_paint_renderer_compositor_tests pp_platform_api_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pp_app_core_about_menu_tests pp_app_core_app_preferences_tests pp_app_core_app_frame_tests pp_app_core_app_input_tests pp_app_core_app_shutdown_tests pp_app_core_app_startup_tests pp_app_core_app_status_tests pp_app_core_command_convert_tests pp_app_core_brush_package_export_tests pp_app_core_brush_package_import_tests pp_app_core_brush_ui_tests pp_app_core_canvas_hotkey_tests pp_app_core_canvas_tool_ui_tests pp_app_core_canvas_view_tests pp_app_core_document_animation_tests pp_app_core_document_canvas_tests pp_app_core_document_cloud_tests pp_app_core_document_export_tests pp_app_core_document_import_tests pp_app_core_document_layer_tests pp_app_core_document_platform_io_tests pp_app_core_document_recording_tests pp_app_core_document_resize_tests pp_app_core_document_route_tests pp_app_core_document_sharing_tests pp_app_core_document_session_tests pp_app_core_file_menu_tests pp_app_core_grid_ui_tests pp_app_core_history_ui_tests pp_app_core_main_toolbar_tests pp_app_core_quick_ui_tests pp_app_core_tools_menu_tests}" start="$(date +%s)" overall_exit=0 diff --git a/src/app_core/app_input.h b/src/app_core/app_input.h new file mode 100644 index 0000000..9a740a5 --- /dev/null +++ b/src/app_core/app_input.h @@ -0,0 +1,166 @@ +#pragma once + +#include "foundation/result.h" + +#include + +namespace pp::app { + +struct AppPointerDispatchPlan { + bool request_redraw = true; + bool dispatch_designer_first = false; + bool dispatch_main_if_not_consumed = false; + float normalized_x = 0.0F; + float normalized_y = 0.0F; +}; + +struct AppInputDispatchPlan { + bool request_redraw = true; + bool dispatch_main = false; +}; + +struct AppGestureDispatchPlan { + bool request_redraw = true; + bool dispatch_main = false; + float normalized_x = 0.0F; + float normalized_y = 0.0F; + float distance = 0.0F; + float distance_delta = 0.0F; + float position_delta_x = 0.0F; + float position_delta_y = 0.0F; +}; + +struct AppKeyDispatchPlan { + bool request_redraw = true; + bool dispatch_main = false; + bool set_key_down = false; + bool sync_vr_camera_rotation = false; +}; + +[[nodiscard]] inline pp::foundation::Status validate_input_zoom(float zoom) +{ + if (!std::isfinite(zoom) || zoom <= 0.0F) { + return pp::foundation::Status::invalid_argument("input zoom must be finite and positive"); + } + + return pp::foundation::Status::success(); +} + +[[nodiscard]] inline pp::foundation::Result plan_app_pointer_dispatch( + float x, + float y, + float zoom, + bool has_designer_layout, + bool has_main_layout) +{ + const auto zoom_status = validate_input_zoom(zoom); + if (!zoom_status.ok()) { + return pp::foundation::Result::failure(zoom_status); + } + + if (!std::isfinite(x) || !std::isfinite(y)) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("input coordinates must be finite")); + } + + return pp::foundation::Result::success(AppPointerDispatchPlan { + .request_redraw = true, + .dispatch_designer_first = has_designer_layout, + .dispatch_main_if_not_consumed = has_main_layout, + .normalized_x = x / zoom, + .normalized_y = y / zoom, + }); +} + +[[nodiscard]] constexpr AppPointerDispatchPlan plan_app_mouse_cancel_dispatch( + bool has_designer_layout, + bool has_main_layout) noexcept +{ + return AppPointerDispatchPlan { + .request_redraw = true, + .dispatch_designer_first = has_designer_layout, + .dispatch_main_if_not_consumed = has_main_layout, + .normalized_x = 0.0F, + .normalized_y = 0.0F, + }; +} + +[[nodiscard]] constexpr AppInputDispatchPlan plan_app_main_input_dispatch(bool has_main_layout) noexcept +{ + return AppInputDispatchPlan { + .request_redraw = true, + .dispatch_main = has_main_layout, + }; +} + +[[nodiscard]] inline pp::foundation::Result plan_app_gesture_dispatch( + float x0, + float y0, + float x1, + float y1, + float previous_x0, + float previous_y0, + float previous_x1, + float previous_y1, + float zoom, + bool has_main_layout) +{ + const auto zoom_status = validate_input_zoom(zoom); + if (!zoom_status.ok()) { + return pp::foundation::Result::failure(zoom_status); + } + + if (!std::isfinite(x0) || !std::isfinite(y0) || !std::isfinite(x1) || !std::isfinite(y1) + || !std::isfinite(previous_x0) || !std::isfinite(previous_y0) + || !std::isfinite(previous_x1) || !std::isfinite(previous_y1)) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("gesture coordinates must be finite")); + } + + const float midpoint_x = (x0 + x1) * 0.5F; + const float midpoint_y = (y0 + y1) * 0.5F; + const float previous_midpoint_x = (previous_x0 + previous_x1) * 0.5F; + const float previous_midpoint_y = (previous_y0 + previous_y1) * 0.5F; + const float dx = x1 - x0; + const float dy = y1 - y0; + const float previous_dx = previous_x1 - previous_x0; + const float previous_dy = previous_y1 - previous_y0; + const float distance = std::sqrt(dx * dx + dy * dy); + const float previous_distance = std::sqrt(previous_dx * previous_dx + previous_dy * previous_dy); + + return pp::foundation::Result::success(AppGestureDispatchPlan { + .request_redraw = true, + .dispatch_main = has_main_layout, + .normalized_x = midpoint_x / zoom, + .normalized_y = midpoint_y / zoom, + .distance = distance, + .distance_delta = distance - previous_distance, + .position_delta_x = midpoint_x - previous_midpoint_x, + .position_delta_y = midpoint_y - previous_midpoint_y, + }); +} + +[[nodiscard]] constexpr AppKeyDispatchPlan plan_app_key_down_dispatch( + bool has_main_layout, + bool is_spacebar, + bool vr_active) noexcept +{ + return AppKeyDispatchPlan { + .request_redraw = true, + .dispatch_main = has_main_layout, + .set_key_down = true, + .sync_vr_camera_rotation = is_spacebar && vr_active, + }; +} + +[[nodiscard]] constexpr AppKeyDispatchPlan plan_app_key_up_dispatch(bool has_main_layout) noexcept +{ + return AppKeyDispatchPlan { + .request_redraw = true, + .dispatch_main = has_main_layout, + .set_key_down = false, + .sync_vr_camera_rotation = false, + }; +} + +} // namespace pp::app diff --git a/src/app_events.cpp b/src/app_events.cpp index 55bbd22..1c5871c 100644 --- a/src/app_events.cpp +++ b/src/app_events.cpp @@ -1,6 +1,7 @@ #include "pch.h" #include "app.h" #include "app_core/app_frame.h" +#include "app_core/app_input.h" #include "app_core/document_platform_io.h" #include "app_core/document_sharing.h" #include "platform_api/platform_services.h" @@ -411,164 +412,249 @@ void App::save_prepared_file( bool App::mouse_down(int button, float x, float y, float pressure, kEventSource source, bool eraser) { - redraw = true; + const auto plan = pp::app::plan_app_pointer_dispatch( + x, + y, + zoom, + layout_designer.get(main_id) != nullptr, + layout.get(main_id) != nullptr); + if (!plan) { + LOG("Mouse down dispatch plan failed: %s", plan.status().message); + return false; + } + + redraw = plan.value().request_redraw; MouseEvent e; e.m_type = button ? kEventType::MouseDownR : kEventType::MouseDownL; - e.m_pos = { x / zoom, y / zoom }; + e.m_pos = { plan.value().normalized_x, plan.value().normalized_y }; e.m_pressure = pressure; e.m_source = source; e.m_eraser = eraser; kEventResult ret = kEventResult::Available; - if (auto* main = layout_designer[main_id]) + if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main) return main->on_event(&e) == kEventResult::Consumed; - if (auto* main = layout[main_id]) + if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main) ret = main->on_event(&e); return ret == kEventResult::Consumed; } bool App::mouse_move(float x, float y, float pressure, kEventSource source, bool eraser) { - cursor = { x / zoom, y / zoom }; - redraw = true; + const auto plan = pp::app::plan_app_pointer_dispatch( + x, + y, + zoom, + layout_designer.get(main_id) != nullptr, + layout.get(main_id) != nullptr); + if (!plan) { + LOG("Mouse move dispatch plan failed: %s", plan.status().message); + return false; + } + + cursor = { plan.value().normalized_x, plan.value().normalized_y }; + redraw = plan.value().request_redraw; MouseEvent e; e.m_type = kEventType::MouseMove; - e.m_pos = { x / zoom, y / zoom }; + e.m_pos = { plan.value().normalized_x, plan.value().normalized_y }; e.m_pressure = pressure; e.m_source = source; e.m_eraser = eraser; kEventResult ret = kEventResult::Available; - if (auto* main = layout_designer[main_id]) + if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main) return main->on_event(&e) == kEventResult::Consumed; - if (auto* main = layout[main_id]) + if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main) ret = main->on_event(&e); return ret == kEventResult::Consumed; } bool App::mouse_up(int button, float x, float y, kEventSource source, bool eraser) { - redraw = true; + const auto plan = pp::app::plan_app_pointer_dispatch( + x, + y, + zoom, + layout_designer.get(main_id) != nullptr, + layout.get(main_id) != nullptr); + if (!plan) { + LOG("Mouse up dispatch plan failed: %s", plan.status().message); + return false; + } + + redraw = plan.value().request_redraw; MouseEvent e; e.m_type = button ? kEventType::MouseUpR : kEventType::MouseUpL; - e.m_pos = { x / zoom, y / zoom }; + e.m_pos = { plan.value().normalized_x, plan.value().normalized_y }; e.m_source = source; e.m_eraser = eraser; kEventResult ret = kEventResult::Available; - if (auto* main = layout_designer[main_id]) + if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main) return main->on_event(&e) == kEventResult::Consumed; - if (auto* main = layout[main_id]) + if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main) ret = main->on_event(&e); return ret == kEventResult::Consumed; } bool App::mouse_scroll(float x, float y, float delta) { - redraw = true; + const auto plan = pp::app::plan_app_pointer_dispatch( + x, + y, + zoom, + layout_designer.get(main_id) != nullptr, + layout.get(main_id) != nullptr); + if (!plan) { + LOG("Mouse scroll dispatch plan failed: %s", plan.status().message); + return false; + } + + redraw = plan.value().request_redraw; MouseEvent e; e.m_type = kEventType::MouseScroll; - e.m_pos = { x / zoom, y / zoom }; + e.m_pos = { plan.value().normalized_x, plan.value().normalized_y }; e.m_scroll_delta = delta; kEventResult ret = kEventResult::Available; - if (auto* main = layout_designer[main_id]) + if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main) return main->on_event(&e) == kEventResult::Consumed; - if (auto* main = layout[main_id]) + if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main) ret = main->on_event(&e); return ret == kEventResult::Consumed; } bool App::mouse_cancel(int button) { - redraw = true; + const auto plan = pp::app::plan_app_mouse_cancel_dispatch( + layout_designer.get(main_id) != nullptr, + layout.get(main_id) != nullptr); + redraw = plan.request_redraw; MouseEvent e; e.m_type = kEventType::MouseCancel; kEventResult ret = kEventResult::Available; - if (auto* main = layout_designer[main_id]) + if (auto* main = layout_designer[main_id]; plan.dispatch_designer_first && main) return main->on_event(&e) == kEventResult::Consumed; - if (auto* main = layout[main_id]) + if (auto* main = layout[main_id]; plan.dispatch_main_if_not_consumed && main) ret = main->on_event(&e); return ret == kEventResult::Consumed; } bool App::gesture_start(const glm::vec2& p0, const glm::vec2& p1) { - redraw = true; + const auto plan = pp::app::plan_app_gesture_dispatch( + p0.x, + p0.y, + p1.x, + p1.y, + p0.x, + p0.y, + p1.x, + p1.y, + zoom, + layout.get(main_id) != nullptr); + if (!plan) { + LOG("Gesture start dispatch plan failed: %s", plan.status().message); + return false; + } + + redraw = plan.value().request_redraw; GestureEvent e; - glm::vec2 p = glm::lerp(p0, p1, 0.5f); e.m_type = kEventType::GestureStart; - e.m_pos = p / glm::vec2(zoom); - e.m_distance = glm::distance(p0, p1); + e.m_pos = { plan.value().normalized_x, plan.value().normalized_y }; + e.m_distance = plan.value().distance; gesture_p0 = p0; gesture_p1 = p1; kEventResult ret = kEventResult::Available; - if (auto* main = layout[main_id]) + if (auto* main = layout[main_id]; plan.value().dispatch_main && main) ret = main->on_event(&e); return ret == kEventResult::Consumed; } bool App::gesture_move(const glm::vec2& p0, const glm::vec2& p1) { - redraw = true; + const auto plan = pp::app::plan_app_gesture_dispatch( + p0.x, + p0.y, + p1.x, + p1.y, + gesture_p0.x, + gesture_p0.y, + gesture_p1.x, + gesture_p1.y, + zoom, + layout.get(main_id) != nullptr); + if (!plan) { + LOG("Gesture move dispatch plan failed: %s", plan.status().message); + return false; + } + + redraw = plan.value().request_redraw; GestureEvent e; - glm::vec2 p = glm::lerp(p0, p1, 0.5f); e.m_type = kEventType::GestureMove; - e.m_pos = p / glm::vec2(zoom); - e.m_distance = glm::distance(p0, p1); - e.m_distance_delta = e.m_distance - glm::distance(gesture_p0, gesture_p1); - e.m_pos_delta = p - glm::lerp(gesture_p0, gesture_p1, 0.5f); + e.m_pos = { plan.value().normalized_x, plan.value().normalized_y }; + e.m_distance = plan.value().distance; + e.m_distance_delta = plan.value().distance_delta; + e.m_pos_delta = { plan.value().position_delta_x, plan.value().position_delta_y }; kEventResult ret = kEventResult::Available; - if (auto* main = layout[main_id]) + if (auto* main = layout[main_id]; plan.value().dispatch_main && main) ret = main->on_event(&e); return ret == kEventResult::Consumed; } bool App::gesture_end() { - redraw = true; + const auto plan = pp::app::plan_app_main_input_dispatch(layout.get(main_id) != nullptr); + redraw = plan.request_redraw; GestureEvent e; e.m_type = kEventType::GestureEnd; kEventResult ret = kEventResult::Available; - if (auto* main = layout[main_id]) + if (auto* main = layout[main_id]; plan.dispatch_main && main) ret = main->on_event(&e); return ret == kEventResult::Consumed; } bool App::touch_tap(const glm::vec2& pos, int fingers, int tap_count) { - redraw = true; + const auto plan = pp::app::plan_app_main_input_dispatch(layout.get(main_id) != nullptr); + redraw = plan.request_redraw; TouchEvent e; e.m_type = kEventType::TouchTap; e.m_finger_count = fingers; e.m_tap_count = tap_count; kEventResult ret = kEventResult::Available; - if (auto* main = layout[main_id]) + if (auto* main = layout[main_id]; plan.dispatch_main && main) ret = main->on_event(&e); return ret == kEventResult::Consumed; } bool App::key_down(kKey key) { - if (key == kKey::KeySpacebar && vr_active) + const auto plan = pp::app::plan_app_key_down_dispatch( + layout.get(main_id) != nullptr, + key == kKey::KeySpacebar, + vr_active); + if (plan.sync_vr_camera_rotation) canvas->m_canvas->m_cam_rot = vr_rot; - redraw = true; - keys[(int)key] = true; + redraw = plan.request_redraw; + keys[(int)key] = plan.set_key_down; KeyEvent e; e.m_type = kEventType::KeyDown; e.m_key = key; kEventResult ret = kEventResult::Available; - if (auto* main = layout[main_id]) + if (auto* main = layout[main_id]; plan.dispatch_main && main) ret = main->on_event(&e); return ret == kEventResult::Consumed; } bool App::key_up(kKey key) { - redraw = true; - keys[(int)key] = false; + const auto plan = pp::app::plan_app_key_up_dispatch(layout.get(main_id) != nullptr); + redraw = plan.request_redraw; + keys[(int)key] = plan.set_key_down; KeyEvent e; e.m_type = kEventType::KeyUp; e.m_key = key; kEventResult ret = kEventResult::Available; - if (auto* main = layout[main_id]) + if (auto* main = layout[main_id]; plan.dispatch_main && main) ret = main->on_event(&e); return ret == kEventResult::Consumed; } bool App::key_char(char key) { - redraw = true; + const auto plan = pp::app::plan_app_main_input_dispatch(layout.get(main_id) != nullptr); + redraw = plan.request_redraw; KeyEvent e; e.m_type = kEventType::KeyChar; e.m_char = key; kEventResult ret = kEventResult::Available; - if (auto* main = layout[main_id]) + if (auto* main = layout[main_id]; plan.dispatch_main && main) ret = main->on_event(&e); return ret == kEventResult::Consumed; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8b2d7c7..9f22246 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -565,6 +565,16 @@ add_test(NAME pp_app_core_app_frame_tests COMMAND pp_app_core_app_frame_tests) set_tests_properties(pp_app_core_app_frame_tests PROPERTIES LABELS "app;desktop-fast;fuzz") +add_executable(pp_app_core_app_input_tests + app_core/app_input_tests.cpp) +target_link_libraries(pp_app_core_app_input_tests PRIVATE + pp_app_core + pp_test_harness) + +add_test(NAME pp_app_core_app_input_tests COMMAND pp_app_core_app_input_tests) +set_tests_properties(pp_app_core_app_input_tests PROPERTIES + LABELS "app;desktop-fast;fuzz") + add_executable(pp_app_core_app_shutdown_tests app_core/app_shutdown_tests.cpp) target_link_libraries(pp_app_core_app_shutdown_tests PRIVATE @@ -1002,6 +1012,31 @@ if(TARGET pano_cli) WILL_FAIL TRUE PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-frame\".*\"message\":\"resize dimensions") + add_test(NAME pano_cli_plan_app_input_pointer_smoke + COMMAND pano_cli plan-app-input --kind pointer --x 100 --y 50 --zoom 2) + set_tests_properties(pano_cli_plan_app_input_pointer_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-input\".*\"kind\":\"pointer\".*\"dispatchDesignerFirst\":true.*\"dispatchMainIfNotConsumed\":true.*\"normalizedX\":50.*\"normalizedY\":25") + + add_test(NAME pano_cli_plan_app_input_gesture_smoke + COMMAND pano_cli plan-app-input --kind gesture --x 0 --y 0 --x1 6 --y1 8 --prev-x 0 --prev-y 0 --prev-x1 3 --prev-y1 4 --zoom 2) + set_tests_properties(pano_cli_plan_app_input_gesture_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-input\".*\"kind\":\"gesture\".*\"dispatchMain\":true.*\"normalizedX\":1.5.*\"normalizedY\":2.*\"distance\":10.*\"distanceDelta\":5") + + add_test(NAME pano_cli_plan_app_input_key_vr_smoke + COMMAND pano_cli plan-app-input --kind key --spacebar --vr-active) + set_tests_properties(pano_cli_plan_app_input_key_vr_smoke PROPERTIES + 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_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 + LABELS "app;integration;desktop-fast;fuzz" + 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_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 new file mode 100644 index 0000000..41b1e9b --- /dev/null +++ b/tests/app_core/app_input_tests.cpp @@ -0,0 +1,118 @@ +#include "app_core/app_input.h" +#include "test_harness.h" + +#include + +namespace { + +void pointer_plan_normalizes_coordinates_and_routes_designer_first(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_app_pointer_dispatch(100.0F, 50.0F, 2.0F, true, true); + + PP_EXPECT(harness, plan); + PP_EXPECT(harness, plan.value().request_redraw); + PP_EXPECT(harness, plan.value().dispatch_designer_first); + PP_EXPECT(harness, plan.value().dispatch_main_if_not_consumed); + PP_EXPECT(harness, plan.value().normalized_x == 50.0F); + PP_EXPECT(harness, plan.value().normalized_y == 25.0F); +} + +void pointer_plan_rejects_invalid_zoom_or_coordinates(pp::tests::Harness& harness) +{ + PP_EXPECT(harness, !pp::app::plan_app_pointer_dispatch(100.0F, 50.0F, 0.0F, true, true)); + PP_EXPECT(harness, !pp::app::plan_app_pointer_dispatch(std::nanf(""), 50.0F, 1.0F, true, true)); +} + +void mouse_cancel_preserves_designer_first_routing(pp::tests::Harness& harness) +{ + const auto designer = pp::app::plan_app_mouse_cancel_dispatch(true, true); + const auto main = pp::app::plan_app_mouse_cancel_dispatch(false, true); + + PP_EXPECT(harness, designer.request_redraw); + PP_EXPECT(harness, designer.dispatch_designer_first); + PP_EXPECT(harness, designer.dispatch_main_if_not_consumed); + PP_EXPECT(harness, !main.dispatch_designer_first); + PP_EXPECT(harness, main.dispatch_main_if_not_consumed); +} + +void gesture_plan_computes_midpoint_distance_and_delta(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_app_gesture_dispatch( + 0.0F, + 0.0F, + 6.0F, + 8.0F, + 0.0F, + 0.0F, + 3.0F, + 4.0F, + 2.0F, + true); + + PP_EXPECT(harness, plan); + PP_EXPECT(harness, plan.value().dispatch_main); + PP_EXPECT(harness, plan.value().normalized_x == 1.5F); + PP_EXPECT(harness, plan.value().normalized_y == 2.0F); + PP_EXPECT(harness, plan.value().distance == 10.0F); + PP_EXPECT(harness, plan.value().distance_delta == 5.0F); + PP_EXPECT(harness, plan.value().position_delta_x == 1.5F); + PP_EXPECT(harness, plan.value().position_delta_y == 2.0F); +} + +void gesture_plan_rejects_invalid_input(pp::tests::Harness& harness) +{ + PP_EXPECT( + harness, + !pp::app::plan_app_gesture_dispatch( + 0.0F, + 0.0F, + 1.0F, + 1.0F, + 0.0F, + 0.0F, + 1.0F, + 1.0F, + -1.0F, + true)); +} + +void key_plan_tracks_state_and_vr_spacebar_sync(pp::tests::Harness& harness) +{ + const auto down = pp::app::plan_app_key_down_dispatch(true, true, true); + const auto ordinary = pp::app::plan_app_key_down_dispatch(true, false, true); + const auto up = pp::app::plan_app_key_up_dispatch(true); + + PP_EXPECT(harness, down.dispatch_main); + PP_EXPECT(harness, down.set_key_down); + PP_EXPECT(harness, down.sync_vr_camera_rotation); + PP_EXPECT(harness, ordinary.set_key_down); + PP_EXPECT(harness, !ordinary.sync_vr_camera_rotation); + PP_EXPECT(harness, up.dispatch_main); + PP_EXPECT(harness, !up.set_key_down); +} + +void simple_input_plan_tracks_main_layout_availability(pp::tests::Harness& harness) +{ + const auto available = pp::app::plan_app_main_input_dispatch(true); + const auto missing = pp::app::plan_app_main_input_dispatch(false); + + PP_EXPECT(harness, available.request_redraw); + PP_EXPECT(harness, available.dispatch_main); + PP_EXPECT(harness, missing.request_redraw); + PP_EXPECT(harness, !missing.dispatch_main); +} + +} // namespace + +int main() +{ + pp::tests::Harness harness; + harness.run("pointer plan normalizes coordinates and routes designer first", pointer_plan_normalizes_coordinates_and_routes_designer_first); + harness.run("pointer plan rejects invalid zoom or coordinates", pointer_plan_rejects_invalid_zoom_or_coordinates); + harness.run("mouse cancel preserves designer first routing", mouse_cancel_preserves_designer_first_routing); + harness.run("gesture plan computes midpoint distance and delta", gesture_plan_computes_midpoint_distance_and_delta); + 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); + return harness.finish(); +} diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index e53256c..988f96c 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -1,6 +1,7 @@ #include "app_core/about_menu.h" #include "app_core/app_preferences.h" #include "app_core/app_frame.h" +#include "app_core/app_input.h" #include "app_core/app_shutdown.h" #include "app_core/app_status.h" #include "app_core/app_startup.h" @@ -263,6 +264,25 @@ struct PlanAppFrameArgs { bool bad_resize = false; }; +struct PlanAppInputArgs { + std::string kind = "pointer"; + float x0 = 100.0F; + float y0 = 50.0F; + float x1 = 160.0F; + float y1 = 130.0F; + float previous_x0 = 100.0F; + float previous_y0 = 50.0F; + float previous_x1 = 130.0F; + float previous_y1 = 90.0F; + float zoom = 2.0F; + bool has_designer_layout = true; + bool has_main_layout = true; + bool spacebar = false; + bool vr_active = false; + bool key_up = false; + bool bad_float = false; +}; + struct PlanCommandConvertArgs { std::string project_path = "D:/Paint/demo.ppi"; std::string output_path = "D:/Paint/demo.png"; @@ -2051,6 +2071,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-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" @@ -3860,6 +3881,199 @@ int plan_app_frame(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_app_input_args( + int argc, + char** argv, + PlanAppInputArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + auto parse_next_float = [&](float& target) -> pp::foundation::Status { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = parse_float_arg(argv[++i]); + if (!value) { + return value.status(); + } + target = value.value(); + return pp::foundation::Status::success(); + }; + + if (key == "--kind") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + args.kind = argv[++i]; + } else if (key == "--x") { + const auto status = parse_next_float(args.x0); + if (!status.ok()) { + return status; + } + } else if (key == "--y") { + const auto status = parse_next_float(args.y0); + if (!status.ok()) { + return status; + } + } else if (key == "--x1") { + const auto status = parse_next_float(args.x1); + if (!status.ok()) { + return status; + } + } else if (key == "--y1") { + const auto status = parse_next_float(args.y1); + if (!status.ok()) { + return status; + } + } else if (key == "--prev-x") { + const auto status = parse_next_float(args.previous_x0); + if (!status.ok()) { + return status; + } + } else if (key == "--prev-y") { + const auto status = parse_next_float(args.previous_y0); + if (!status.ok()) { + return status; + } + } else if (key == "--prev-x1") { + const auto status = parse_next_float(args.previous_x1); + if (!status.ok()) { + return status; + } + } else if (key == "--prev-y1") { + const auto status = parse_next_float(args.previous_y1); + if (!status.ok()) { + return status; + } + } else if (key == "--zoom") { + const auto status = parse_next_float(args.zoom); + if (!status.ok()) { + return status; + } + } else if (key == "--no-designer-layout") { + args.has_designer_layout = false; + } else if (key == "--no-main-layout") { + args.has_main_layout = false; + } else if (key == "--spacebar") { + args.spacebar = true; + } else if (key == "--vr-active") { + args.vr_active = true; + } else if (key == "--key-up") { + args.key_up = true; + } else if (key == "--bad-float") { + args.bad_float = true; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_app_input(int argc, char** argv) +{ + PlanAppInputArgs args; + const auto status = parse_plan_app_input_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-app-input", status.message); + return 2; + } + + if (args.bad_float) { + args.zoom = 0.0F; + } + + if (args.kind == "pointer") { + const auto plan = pp::app::plan_app_pointer_dispatch( + args.x0, + args.y0, + args.zoom, + args.has_designer_layout, + args.has_main_layout); + if (!plan) { + print_error("plan-app-input", plan.status().message); + return 2; + } + std::cout << "{\"ok\":true,\"command\":\"plan-app-input\",\"kind\":\"pointer\"" + << ",\"requestRedraw\":" << json_bool(plan.value().request_redraw) + << ",\"dispatchDesignerFirst\":" << json_bool(plan.value().dispatch_designer_first) + << ",\"dispatchMainIfNotConsumed\":" << json_bool(plan.value().dispatch_main_if_not_consumed) + << ",\"normalizedX\":" << plan.value().normalized_x + << ",\"normalizedY\":" << plan.value().normalized_y + << "}\n"; + return 0; + } + + if (args.kind == "gesture") { + const auto plan = pp::app::plan_app_gesture_dispatch( + args.x0, + args.y0, + args.x1, + args.y1, + args.previous_x0, + args.previous_y0, + args.previous_x1, + args.previous_y1, + args.zoom, + args.has_main_layout); + if (!plan) { + print_error("plan-app-input", plan.status().message); + return 2; + } + std::cout << "{\"ok\":true,\"command\":\"plan-app-input\",\"kind\":\"gesture\"" + << ",\"requestRedraw\":" << json_bool(plan.value().request_redraw) + << ",\"dispatchMain\":" << json_bool(plan.value().dispatch_main) + << ",\"normalizedX\":" << plan.value().normalized_x + << ",\"normalizedY\":" << plan.value().normalized_y + << ",\"distance\":" << plan.value().distance + << ",\"distanceDelta\":" << plan.value().distance_delta + << ",\"positionDeltaX\":" << plan.value().position_delta_x + << ",\"positionDeltaY\":" << plan.value().position_delta_y + << "}\n"; + return 0; + } + + if (args.kind == "cancel") { + const auto plan = pp::app::plan_app_mouse_cancel_dispatch( + args.has_designer_layout, + args.has_main_layout); + std::cout << "{\"ok\":true,\"command\":\"plan-app-input\",\"kind\":\"cancel\"" + << ",\"requestRedraw\":" << json_bool(plan.request_redraw) + << ",\"dispatchDesignerFirst\":" << json_bool(plan.dispatch_designer_first) + << ",\"dispatchMainIfNotConsumed\":" << json_bool(plan.dispatch_main_if_not_consumed) + << "}\n"; + return 0; + } + + if (args.kind == "main") { + const auto plan = pp::app::plan_app_main_input_dispatch(args.has_main_layout); + std::cout << "{\"ok\":true,\"command\":\"plan-app-input\",\"kind\":\"main\"" + << ",\"requestRedraw\":" << json_bool(plan.request_redraw) + << ",\"dispatchMain\":" << json_bool(plan.dispatch_main) + << "}\n"; + return 0; + } + + if (args.kind == "key") { + const auto down = pp::app::plan_app_key_down_dispatch( + args.has_main_layout, + args.spacebar, + args.vr_active); + const auto up = pp::app::plan_app_key_up_dispatch(args.has_main_layout); + const auto& plan = args.key_up ? up : down; + std::cout << "{\"ok\":true,\"command\":\"plan-app-input\",\"kind\":\"key\"" + << ",\"requestRedraw\":" << json_bool(plan.request_redraw) + << ",\"dispatchMain\":" << json_bool(plan.dispatch_main) + << ",\"setKeyDown\":" << json_bool(plan.set_key_down) + << ",\"syncVrCameraRotation\":" << json_bool(plan.sync_vr_camera_rotation) + << "}\n"; + return 0; + } + + print_error("plan-app-input", "unknown input plan kind"); + return 2; +} + int plan_app_shutdown(int argc, char** argv) { static_cast(argv); @@ -10202,6 +10416,10 @@ int main(int argc, char** argv) return plan_app_frame(argc, argv); } + if (command == "plan-app-input") { + return plan_app_input(argc, argv); + } + if (command == "plan-app-shutdown") { return plan_app_shutdown(argc, argv); }