Extend app input planning to UI state

This commit is contained in:
2026-06-05 06:44:57 +02:00
parent b825d920d2
commit 32c95b224f
8 changed files with 196 additions and 19 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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`,

View File

@@ -3,6 +3,7 @@
#include "foundation/result.h"
#include <cmath>
#include <cstddef>
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<AppUiVisibilityTogglePlan> 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<AppUiVisibilityTogglePlan>::failure(
pp::foundation::Status::invalid_argument("UI toggle requires a main layout"));
}
if (main_child_count <= 1U) {
return pp::foundation::Result<AppUiVisibilityTogglePlan>::failure(
pp::foundation::Status::invalid_argument("UI toggle requires a panel container child"));
}
return pp::foundation::Result<AppUiVisibilityTogglePlan>::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

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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;
}