Plan clipboard text actions in app core

This commit is contained in:
2026-06-03 03:52:00 +02:00
parent 5ee2dd271c
commit 6960bd3410
9 changed files with 162 additions and 2 deletions

View File

@@ -440,6 +440,10 @@ Known local toolchain state:
- `pano_cli plan-cursor-visibility` exposes `pp_app_core` cursor visibility - `pano_cli plan-cursor-visibility` exposes `pp_app_core` cursor visibility
planning as JSON for hidden and visible states; live canvas cursor requests planning as JSON for hidden and visible states; live canvas cursor requests
consume the same contract before retained desktop platform cursor bridges. consume the same contract before retained desktop platform cursor bridges.
- `pano_cli plan-clipboard-read` and `pano_cli plan-clipboard-write` expose
`pp_app_core` clipboard text planning as JSON, including empty text writes;
live clipboard get/set requests consume the same contracts before retained
platform clipboard bridges.
- `pano_cli plan-cloud-upload` exposes `pp_app_core` cloud upload availability, - `pano_cli plan-cloud-upload` exposes `pp_app_core` cloud upload availability,
new-document warning, publish prompt, and save-before-upload planning as JSON; new-document warning, publish prompt, and save-before-upload planning as JSON;
the live cloud upload command consumes the same start contract before the live cloud upload command consumes the same start contract before
@@ -474,7 +478,8 @@ Known local toolchain state:
and non-empty picked-path callback planning, plus empty/non-empty display-file and non-empty picked-path callback planning, plus empty/non-empty display-file
planning before platform picker/display callbacks, plus virtual keyboard planning before platform picker/display callbacks, plus virtual keyboard
show/hide planning before platform keyboard callbacks, plus cursor visibility show/hide planning before platform keyboard callbacks, plus cursor visibility
planning before platform cursor callbacks. planning before platform cursor callbacks, plus clipboard read/write
planning before platform clipboard callbacks.
- `pp_app_core_document_cloud_tests` covers cloud upload no-canvas, - `pp_app_core_document_cloud_tests` covers cloud upload no-canvas,
new-document warning, clean publish prompt, and dirty save-before-upload new-document warning, clean publish prompt, and dirty save-before-upload
decisions, plus cloud browse no-canvas/show-browser and selected-download decisions, plus cloud browse no-canvas/show-browser and selected-download

View File

@@ -67,7 +67,7 @@ and validation command.
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Mouse/keyboard/touch/gestures/cursor | `App`, platform entrypoints | `pp_app_core`, `pp_platform_*`, app | Cursor visibility decision tests, 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 | | 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 | | Clipboard/file picker/share/display | `App` platform methods | `pp_app_core`, `pp_platform_*` | Clipboard read/write, 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 | | Virtual keyboard | `App`, platform entrypoints | `pp_app_core`, `pp_platform_*` | Keyboard visibility decision tests, platform smoke |
| OpenVR desktop | `HMD`, `Vive`, `app_vr` | `pp_platform_vr`, app | Compile gate and mocked pose tests | | OpenVR desktop | `HMD`, `Vive`, `app_vr` | `pp_platform_vr`, app | Compile gate and mocked pose tests |
| Quest/OVR | Android Quest files | `pp_platform_android_quest` | Compile/package gate | | Quest/OVR | Android Quest files | `pp_platform_android_quest` | Compile/package gate |

View File

@@ -34,6 +34,7 @@ agent or engineer to remove them without reconstructing context from chat.
| 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-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-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 | | 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 |
| DEBT-0016 | Open | Modernization | Clipboard get/set requests now consume pure `pp_app_core` planning through `pano_cli plan-clipboard-read` and `pano_cli plan-clipboard-write`, but live clipboard execution still reaches retained Win32/Apple/Android platform bridges from `App::clipboard_get_text` and `App::clipboard_set_text` | Keep picker/color text clipboard behavior stable while platform shells are extracted incrementally | `pp_app_core_document_platform_io_tests`; `pano_cli plan-clipboard-write --text #ff00aa`; `ctest --preset desktop-fast --build-config Debug` | Clipboard 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 ## Closed Debt

View File

@@ -463,6 +463,9 @@ mobile platform keyboard bridges continue.
`pano_cli plan-cursor-visibility` exposes the app-core cursor visibility `pano_cli plan-cursor-visibility` exposes the app-core cursor visibility
decision used by live canvas cursor requests before retained desktop platform decision used by live canvas cursor requests before retained desktop platform
cursor bridges continue. cursor bridges continue.
`pano_cli plan-clipboard-read` and `pano_cli plan-clipboard-write` expose the
app-core clipboard text decisions used by live clipboard get/set requests
before retained platform clipboard bridges continue.
`pano_cli plan-cloud-upload` exposes the app-core cloud upload decision used by `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 the live cloud upload command for missing-canvas, new-document warning, publish
prompt, and dirty-document save-before-upload states before legacy UI, canvas, prompt, and dirty-document save-before-upload states before legacy UI, canvas,
@@ -954,6 +957,7 @@ Results:
callbacks, plus empty/non-empty display-file planning before platform callbacks, plus empty/non-empty display-file planning before platform
display callbacks, plus virtual keyboard show/hide planning before platform display callbacks, plus virtual keyboard show/hide planning before platform
keyboard callbacks, plus cursor visibility planning before platform cursor keyboard callbacks, plus cursor visibility planning before platform cursor
callbacks, plus clipboard read/write planning before platform clipboard
callbacks. callbacks.
- `pano_cli_plan_picked_path_empty_smoke` and - `pano_cli_plan_picked_path_empty_smoke` and
`pano_cli_plan_picked_path_selected_smoke` passed and expose app-core picker `pano_cli_plan_picked_path_selected_smoke` passed and expose app-core picker
@@ -967,6 +971,10 @@ Results:
- `pano_cli_plan_cursor_visibility_hidden_smoke` and - `pano_cli_plan_cursor_visibility_hidden_smoke` and
`pano_cli_plan_cursor_visibility_visible_smoke` passed and expose app-core `pano_cli_plan_cursor_visibility_visible_smoke` passed and expose app-core
cursor visibility decisions as JSON. cursor visibility decisions as JSON.
- `pano_cli_plan_clipboard_read_smoke`,
`pano_cli_plan_clipboard_write_smoke`, and
`pano_cli_plan_clipboard_write_empty_smoke` passed and expose app-core
clipboard decisions as JSON, including empty write text.
- `panopainter_validate_shaders` passed, validating 25 shader programs and 7 - `panopainter_validate_shaders` passed, validating 25 shader programs and 7
shader includes for stage markers and include graph integrity. shader includes for stage markers and include graph integrity.
- `pp_renderer_gl_capabilities_tests` passed on default MSVC, vcpkg-headless, - `pp_renderer_gl_capabilities_tests` passed on default MSVC, vcpkg-headless,

View File

@@ -24,6 +24,14 @@ enum class CursorVisibilityAction {
hide_cursor, hide_cursor,
}; };
enum class ClipboardReadAction {
read_text,
};
enum class ClipboardWriteAction {
write_text,
};
[[nodiscard]] constexpr PickedPathAction plan_picked_path(std::string_view path) noexcept [[nodiscard]] constexpr PickedPathAction plan_picked_path(std::string_view path) noexcept
{ {
return path.empty() return path.empty()
@@ -52,4 +60,14 @@ enum class CursorVisibilityAction {
: CursorVisibilityAction::hide_cursor; : CursorVisibilityAction::hide_cursor;
} }
[[nodiscard]] constexpr ClipboardReadAction plan_clipboard_read() noexcept
{
return ClipboardReadAction::read_text;
}
[[nodiscard]] constexpr ClipboardWriteAction plan_clipboard_write(std::string_view) noexcept
{
return ClipboardWriteAction::write_text;
}
} }

View File

@@ -63,6 +63,9 @@ void webgl_sync();
std::string App::clipboard_get_text() std::string App::clipboard_get_text()
{ {
if (pp::app::plan_clipboard_read() != pp::app::ClipboardReadAction::read_text)
return {};
#if _WIN32 #if _WIN32
return win32_clipboard_get_text(); return win32_clipboard_get_text();
#elif __IOS__ #elif __IOS__
@@ -76,6 +79,9 @@ std::string App::clipboard_get_text()
bool App::clipboard_set_text(const std::string& s) bool App::clipboard_set_text(const std::string& s)
{ {
if (pp::app::plan_clipboard_write(s) != pp::app::ClipboardWriteAction::write_text)
return false;
#if _WIN32 #if _WIN32
return win32_clipboard_set_text(s); return win32_clipboard_set_text(s);
#elif __IOS__ #elif __IOS__

View File

@@ -651,6 +651,24 @@ if(TARGET pano_cli)
LABELS "app;integration;desktop-fast" LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-cursor-visibility\".*\"visible\":true.*\"decision\":\"show-cursor\"") PASS_REGULAR_EXPRESSION "\"command\":\"plan-cursor-visibility\".*\"visible\":true.*\"decision\":\"show-cursor\"")
add_test(NAME pano_cli_plan_clipboard_read_smoke
COMMAND pano_cli plan-clipboard-read)
set_tests_properties(pano_cli_plan_clipboard_read_smoke PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-clipboard-read\".*\"decision\":\"read-text\"")
add_test(NAME pano_cli_plan_clipboard_write_smoke
COMMAND pano_cli plan-clipboard-write --text "#ff00aa")
set_tests_properties(pano_cli_plan_clipboard_write_smoke PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-clipboard-write\".*\"text\":\"#ff00aa\".*\"decision\":\"write-text\"")
add_test(NAME pano_cli_plan_clipboard_write_empty_smoke
COMMAND pano_cli plan-clipboard-write)
set_tests_properties(pano_cli_plan_clipboard_write_empty_smoke PROPERTIES
LABELS "app;integration;desktop-fast;fuzz"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-clipboard-write\".*\"text\":\"\".*\"decision\":\"write-text\"")
add_test(NAME pano_cli_simulate_app_session_clean_smoke add_test(NAME pano_cli_simulate_app_session_clean_smoke
COMMAND pano_cli simulate-app-session) COMMAND pano_cli simulate-app-session)
set_tests_properties(pano_cli_simulate_app_session_clean_smoke PROPERTIES set_tests_properties(pano_cli_simulate_app_session_clean_smoke PROPERTIES

View File

@@ -59,6 +59,27 @@ void cursor_visibility_plans_hide_action(pp::tests::Harness& harness)
pp::app::plan_cursor_visibility(false) == pp::app::CursorVisibilityAction::hide_cursor); pp::app::plan_cursor_visibility(false) == pp::app::CursorVisibilityAction::hide_cursor);
} }
void clipboard_read_plans_text_read(pp::tests::Harness& harness)
{
PP_EXPECT(
harness,
pp::app::plan_clipboard_read() == pp::app::ClipboardReadAction::read_text);
}
void clipboard_write_plans_text_write(pp::tests::Harness& harness)
{
PP_EXPECT(
harness,
pp::app::plan_clipboard_write("#ff00aa") == pp::app::ClipboardWriteAction::write_text);
}
void clipboard_write_keeps_empty_text_writable(pp::tests::Harness& harness)
{
PP_EXPECT(
harness,
pp::app::plan_clipboard_write("") == pp::app::ClipboardWriteAction::write_text);
}
} }
int main() int main()
@@ -72,5 +93,8 @@ int main()
harness.run("virtual keyboard plans hide action", virtual_keyboard_plans_hide_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 show action", cursor_visibility_plans_show_action);
harness.run("cursor visibility plans hide action", cursor_visibility_plans_hide_action); harness.run("cursor visibility plans hide action", cursor_visibility_plans_hide_action);
harness.run("clipboard read plans text read", clipboard_read_plans_text_read);
harness.run("clipboard write plans text write", clipboard_write_plans_text_write);
harness.run("clipboard write keeps empty text writable", clipboard_write_keeps_empty_text_writable);
return harness.finish(); return harness.finish();
} }

View File

@@ -182,6 +182,10 @@ struct PlanCursorVisibilityArgs {
bool visible = false; bool visible = false;
}; };
struct PlanClipboardWriteArgs {
std::string text;
};
struct SimulateAppSessionArgs { struct SimulateAppSessionArgs {
bool has_canvas = true; bool has_canvas = true;
bool new_document = false; bool new_document = false;
@@ -563,6 +567,26 @@ const char* cursor_visibility_action_name(pp::app::CursorVisibilityAction action
return "hide-cursor"; return "hide-cursor";
} }
const char* clipboard_read_action_name(pp::app::ClipboardReadAction action) noexcept
{
switch (action) {
case pp::app::ClipboardReadAction::read_text:
return "read-text";
}
return "read-text";
}
const char* clipboard_write_action_name(pp::app::ClipboardWriteAction action) noexcept
{
switch (action) {
case pp::app::ClipboardWriteAction::write_text:
return "write-text";
}
return "write-text";
}
pp::foundation::Result<float> parse_float_arg(std::string_view text) pp::foundation::Result<float> parse_float_arg(std::string_view text)
{ {
float value = 0.0F; float value = 0.0F;
@@ -608,6 +632,8 @@ void print_help()
<< " plan-display-file [--path FILE]\n" << " plan-display-file [--path FILE]\n"
<< " plan-keyboard-visibility [--visible]\n" << " plan-keyboard-visibility [--visible]\n"
<< " plan-cursor-visibility [--visible]\n" << " plan-cursor-visibility [--visible]\n"
<< " plan-clipboard-read\n"
<< " plan-clipboard-write [--text TEXT]\n"
<< " load-project --path FILE\n" << " load-project --path FILE\n"
<< " parse-layout --path FILE\n" << " parse-layout --path FILE\n"
<< " record-render [--width N] [--height N] [--exercise-clear]\n" << " record-render [--width N] [--height N] [--exercise-clear]\n"
@@ -2120,6 +2146,52 @@ int plan_cursor_visibility(int argc, char** argv)
return 0; return 0;
} }
int plan_clipboard_read(int, char**)
{
const auto decision = pp::app::plan_clipboard_read();
std::cout << "{\"ok\":true,\"command\":\"plan-clipboard-read\""
<< ",\"decision\":\"" << clipboard_read_action_name(decision)
<< "\"}\n";
return 0;
}
pp::foundation::Status parse_plan_clipboard_write_args(
int argc,
char** argv,
PlanClipboardWriteArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--text") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.text = argv[++i];
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
int plan_clipboard_write(int argc, char** argv)
{
PlanClipboardWriteArgs args;
const auto status = parse_plan_clipboard_write_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-clipboard-write", status.message);
return 2;
}
const auto decision = pp::app::plan_clipboard_write(args.text);
std::cout << "{\"ok\":true,\"command\":\"plan-clipboard-write\""
<< ",\"state\":{\"text\":\"" << json_escape(args.text)
<< "\"},\"decision\":\"" << clipboard_write_action_name(decision)
<< "\"}\n";
return 0;
}
pp::foundation::Status parse_plan_export_target_args( pp::foundation::Status parse_plan_export_target_args(
int argc, int argc,
char** argv, char** argv,
@@ -4307,6 +4379,14 @@ int main(int argc, char** argv)
return plan_cursor_visibility(argc, argv); return plan_cursor_visibility(argc, argv);
} }
if (command == "plan-clipboard-read") {
return plan_clipboard_read(argc, argv);
}
if (command == "plan-clipboard-write") {
return plan_clipboard_write(argc, argv);
}
if (command == "load-project") { if (command == "load-project") {
return load_project(argc, argv); return load_project(argc, argv);
} }