diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f3765b..5d717bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -236,7 +236,8 @@ add_library(pp_app_core STATIC src/app_core/document_route.cpp src/app_core/document_sharing.h src/app_core/document_session.cpp - src/app_core/grid_ui.h) + src/app_core/grid_ui.h + src/app_core/quick_ui.h) target_include_directories(pp_app_core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index a77b798..adf83a4 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -42,6 +42,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0022 | Open | Modernization | Animation panel frame command planning now consumes pure `pp_app_core` through `NodePanelAnimation` and `pano_cli plan-animation-operation`, and `pp_legacy_ui_core` temporarily links `pp_app_core`, but live execution still mutates legacy `Canvas`/`Layer` frame state and animation playback state directly | Preserve existing animation panel behavior while timeline/frame commands move toward the document/app command boundary | `pp_app_core_document_animation_tests`; `pano_cli plan-animation-operation --kind add --frame-count 2 --current-frame 0`; `pano_cli plan-animation-operation --kind next --total-duration 5 --current-frame 4`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline execution is owned by the document/app command boundary with legacy `Canvas`/`Layer`/UI nodes acting only as adapters or removed entirely | | DEBT-0023 | Open | Modernization | Brush/color/preset UI planning now consumes pure `pp_app_core` through `App::init_sidebar`, restored/docked floating-panel callbacks, and `pano_cli plan-brush-operation`, but live execution still mutates legacy `Brush`, calls legacy brush texture loading, and refreshes legacy quick/stroke/color widgets directly | Preserve existing brush UI behavior while brush commands move toward a brush/app command boundary and asset-managed texture selection | `pp_app_core_brush_ui_tests`; `pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1`; `pano_cli plan-brush-operation --kind pattern --path data/patterns/noise.png --thumb data/patterns/thumbs/noise.png`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset execution is owned by a brush/app command boundary with legacy `Brush`/UI nodes acting only as adapters or removed entirely | | DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning now consumes pure `pp_app_core` through `NodePanelGrid` and `pano_cli plan-grid-operation`, but live execution still performs legacy image loading, OpenGL texture updates, nanort lightmap baking, progress UI, and `Canvas::draw_objects` commit directly | Preserve grid/lightmap behavior while moving renderable grid commands toward app/renderer/document boundaries | `pp_app_core_grid_ui_tests`; `pano_cli plan-grid-operation --kind render --float32 --texture-resolution 1024 --samples 32`; `ctest --preset desktop-fast --build-config Debug` | Grid heightmap/lightmap execution is owned by app/renderer/document services with `NodePanelGrid` acting only as UI adapter | +| DEBT-0025 | Open | Modernization | Quick brush/color slot and mini-state planning now consumes pure `pp_app_core` through `NodePanelQuick` and `pano_cli plan-quick-operation`, but live execution still mutates legacy quick UI widgets, `Brush` previews, color picker popup state, and preset popup state directly | Preserve quick-panel behavior while quick brush/color commands move toward a brush/app command boundary with safer automation coverage | `pp_app_core_quick_ui_tests`; `pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 2`; `pano_cli plan-quick-operation --kind restore --brush-index 2 --color-index 1 --fire-event`; `ctest --preset desktop-fast --build-config Debug` | Quick-panel selection, popup, restore, reset, brush preview, and color execution are owned by app/brush/UI services with `NodePanelQuick` acting only as UI adapter | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 269bf37..ce350e1 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -503,6 +503,10 @@ callbacks before legacy `Brush` mutation and resource loading continue. pick/load/reload/clear, lightmap render capability/limit checks, and heightmap commit used by the live grid panel before legacy image loading, OpenGL texture updates, nanort lightmap baking, and `Canvas::draw_objects` execution continue. +`pano_cli plan-quick-operation` exposes app-core planning for quick brush/color +slot selection versus popup opening, plus quick mini-state restore/reset +validation used by the live quick panel before legacy `Brush`, color picker, +stroke preview, and preset popup execution continue. `pp_platform_api` now owns a headless `PlatformServices` interface for startup storage path preparation, clipboard text, cursor visibility, virtual-keyboard visibility, UI-thread lifecycle hooks, render-context @@ -1151,6 +1155,16 @@ Results: `pano_cli_plan_grid_operation_rejects_empty_reload`, and `pano_cli_plan_grid_operation_rejects_bad_samples` passed and expose live grid/heightmap/lightmap planning as JSON automation. +- `pp_app_core_quick_ui_tests` passed, covering quick brush/color slot + selection, active-slot popup decisions, invalid slot rejection, restore-state + validation, and reset-state validation. +- `pano_cli_plan_quick_operation_select_brush_smoke`, + `pano_cli_plan_quick_operation_open_color_smoke`, + `pano_cli_plan_quick_operation_restore_smoke`, + `pano_cli_plan_quick_operation_reset_smoke`, + `pano_cli_plan_quick_operation_rejects_bad_slot`, and + `pano_cli_plan_quick_operation_rejects_bad_restore` passed and expose live + quick-panel planning as JSON automation. - `pp_app_core_document_sharing_tests` passed, covering saved-path gating before platform share execution. - `pano_cli_plan_share_file_unsaved_smoke` and diff --git a/src/app_core/quick_ui.h b/src/app_core/quick_ui.h new file mode 100644 index 0000000..6d6d598 --- /dev/null +++ b/src/app_core/quick_ui.h @@ -0,0 +1,143 @@ +#pragma once + +#include "foundation/result.h" + +namespace pp::app { + +enum class QuickUiSlotKind { + brush, + color, +}; + +enum class QuickUiOperation { + select_slot, + open_slot_popup, + restore_state, + reset_state, +}; + +struct QuickUiPlan { + QuickUiOperation operation = QuickUiOperation::select_slot; + QuickUiSlotKind slot_kind = QuickUiSlotKind::brush; + int slot_index = 0; + int previous_index = 0; + int slot_count = 0; + bool fire_event = false; + bool updates_selection = false; + bool opens_brush_popup = false; + bool opens_color_picker = false; + bool invokes_change_callback = false; + bool restores_slots = false; + bool resets_slots = false; + bool redraws_brush_previews = false; + bool mutates_quick_state = false; +}; + +[[nodiscard]] inline pp::foundation::Status validate_quick_slot_count(int slot_count) noexcept +{ + if (slot_count <= 0) { + return pp::foundation::Status::out_of_range("quick slot count must be greater than zero"); + } + + return pp::foundation::Status::success(); +} + +[[nodiscard]] inline pp::foundation::Status validate_quick_slot_index(int slot_index, int slot_count) noexcept +{ + const auto count_status = validate_quick_slot_count(slot_count); + if (!count_status.ok()) { + return count_status; + } + + if (slot_index < 0 || slot_index >= slot_count) { + return pp::foundation::Status::out_of_range("quick slot index is out of range"); + } + + return pp::foundation::Status::success(); +} + +[[nodiscard]] inline pp::foundation::Result plan_quick_slot_click( + QuickUiSlotKind slot_kind, + int current_index, + int clicked_index, + int slot_count) +{ + const auto current_status = validate_quick_slot_index(current_index, slot_count); + if (!current_status.ok()) { + return pp::foundation::Result::failure(current_status); + } + + const auto clicked_status = validate_quick_slot_index(clicked_index, slot_count); + if (!clicked_status.ok()) { + return pp::foundation::Result::failure(clicked_status); + } + + QuickUiPlan plan; + plan.slot_kind = slot_kind; + plan.slot_index = clicked_index; + plan.previous_index = current_index; + plan.slot_count = slot_count; + if (clicked_index != current_index) { + plan.operation = QuickUiOperation::select_slot; + plan.updates_selection = true; + plan.invokes_change_callback = true; + plan.mutates_quick_state = true; + return pp::foundation::Result::success(plan); + } + + plan.operation = QuickUiOperation::open_slot_popup; + plan.opens_brush_popup = slot_kind == QuickUiSlotKind::brush; + plan.opens_color_picker = slot_kind == QuickUiSlotKind::color; + return pp::foundation::Result::success(plan); +} + +[[nodiscard]] inline pp::foundation::Result plan_quick_state_restore( + int brush_index, + int color_index, + int slot_count, + bool fire_event) +{ + const auto brush_status = validate_quick_slot_index(brush_index, slot_count); + if (!brush_status.ok()) { + return pp::foundation::Result::failure(brush_status); + } + + const auto color_status = validate_quick_slot_index(color_index, slot_count); + if (!color_status.ok()) { + return pp::foundation::Result::failure(color_status); + } + + QuickUiPlan plan; + plan.operation = QuickUiOperation::restore_state; + plan.slot_count = slot_count; + plan.fire_event = fire_event; + plan.updates_selection = true; + plan.invokes_change_callback = fire_event; + plan.restores_slots = true; + plan.redraws_brush_previews = true; + plan.mutates_quick_state = true; + return pp::foundation::Result::success(plan); +} + +[[nodiscard]] inline pp::foundation::Result plan_quick_state_reset( + int slot_count, + bool fire_event) +{ + const auto count_status = validate_quick_slot_count(slot_count); + if (!count_status.ok()) { + return pp::foundation::Result::failure(count_status); + } + + QuickUiPlan plan; + plan.operation = QuickUiOperation::reset_state; + plan.slot_count = slot_count; + plan.fire_event = fire_event; + plan.updates_selection = true; + plan.invokes_change_callback = fire_event; + plan.resets_slots = true; + plan.redraws_brush_previews = true; + plan.mutates_quick_state = true; + return pp::foundation::Result::success(plan); +} + +} // namespace pp::app diff --git a/src/node_panel_quick.cpp b/src/node_panel_quick.cpp index 88274db..300a6fb 100644 --- a/src/node_panel_quick.cpp +++ b/src/node_panel_quick.cpp @@ -1,4 +1,5 @@ #include "pch.h" +#include "app_core/quick_ui.h" #include "node_panel_quick.h" #include "node_stroke_preview.h" #include "node_image.h" @@ -31,11 +32,13 @@ void NodePanelQuick::set_color(glm::vec3 color) int NodePanelQuick::get_selected_brush_index() const { auto it = std::find(m_button_brushes.begin(), m_button_brushes.end(), m_button_brush_current); - return std::distance(m_button_brushes.begin(), it); + return static_cast(std::distance(m_button_brushes.begin(), it)); } void NodePanelQuick::set_selected_brush_index(int idx, bool fire_event /*= false*/) { + if (!pp::app::validate_quick_slot_index(idx, static_cast(m_button_brushes.size())).ok()) + return; if (m_button_brush_current) m_button_brush_current->set_active(false); m_button_brush_current = m_button_brushes[idx]; @@ -48,11 +51,13 @@ void NodePanelQuick::set_selected_brush_index(int idx, bool fire_event /*= false int NodePanelQuick::get_selected_color_index() const { auto it = std::find(m_button_colors.begin(), m_button_colors.end(), m_button_color_current); - return std::distance(m_button_colors.begin(), it); + return static_cast(std::distance(m_button_colors.begin(), it)); } void NodePanelQuick::set_selected_color_index(int idx, bool fire_event /*= false*/) { + if (!pp::app::validate_quick_slot_index(idx, static_cast(m_button_colors.size())).ok()) + return; if (m_button_color_current) m_button_color_current->set_active(false); m_button_color_current = m_button_colors[idx]; @@ -77,6 +82,14 @@ NodePanelQuick::MiniState NodePanelQuick::get_state() const void NodePanelQuick::set_state(const MiniState& state, bool fire_event /*= false*/) { + const auto plan = pp::app::plan_quick_state_restore( + state.brush_index, + state.color_index, + static_cast(m_button_brushes.size()), + fire_event); + if (!plan) + return; + for (int i = 0; i < 3; i++) { auto b = static_cast(m_button_brushes[i]->m_children[0].get()); @@ -91,6 +104,10 @@ void NodePanelQuick::set_state(const MiniState& state, bool fire_event /*= false void NodePanelQuick::reset_state(bool fire_event /*= false*/) { + const auto plan = pp::app::plan_quick_state_reset(static_cast(m_button_brushes.size()), fire_event); + if (!plan) + return; + for (int i = 0; i < 3; i++) { auto b = static_cast(m_button_brushes[i]->m_children[0].get()); @@ -179,10 +196,12 @@ NodeButtonCustom* NodePanelQuick::init_button_brush(const std::string& name, boo { LOG("init_button_brush %s", name.c_str()); auto button = find(name.c_str()); - if (!button) + if (!button) { LOG("couldn't find button %s", name.c_str()); + return nullptr; + } button->on_click = std::bind(&this_class::handle_button_brush_click, this, std::placeholders::_1); - LOG("button has %d children", button->m_children.size()); + LOG("button has %d children", static_cast(button->m_children.size())); auto pr = static_cast(button->m_children[0].get()); pr->m_brush = std::make_shared(); pr->m_brush->m_tip_size_pressure = szp; @@ -197,8 +216,17 @@ NodeButtonCustom* NodePanelQuick::init_button_brush(const std::string& name, boo void NodePanelQuick::handle_button_brush_click(Node* button) { - // the first time select the box - if (m_button_brush_current != button) + const auto clicked = std::find(m_button_brushes.begin(), m_button_brushes.end(), button); + const int clicked_index = static_cast(std::distance(m_button_brushes.begin(), clicked)); + const auto plan = pp::app::plan_quick_slot_click( + pp::app::QuickUiSlotKind::brush, + get_selected_brush_index(), + clicked_index, + static_cast(m_button_brushes.size())); + if (!plan) + return; + + if (plan.value().updates_selection) { auto b = static_cast(button); b->set_active(true); @@ -210,7 +238,8 @@ void NodePanelQuick::handle_button_brush_click(Node* button) return; } - // if the box is already selected show the popup + if (!plan.value().opens_brush_popup) + return; auto popup = App::I->presets; auto screen = root()->m_size; @@ -268,8 +297,17 @@ void NodePanelQuick::handle_button_brush_click(Node* button) void NodePanelQuick::handle_button_color_click(Node* target) { - // the first time select the box - if (m_button_color_current != target) + const auto clicked = std::find(m_button_colors.begin(), m_button_colors.end(), target); + const int clicked_index = static_cast(std::distance(m_button_colors.begin(), clicked)); + const auto plan = pp::app::plan_quick_slot_click( + pp::app::QuickUiSlotKind::color, + get_selected_color_index(), + clicked_index, + static_cast(m_button_colors.size())); + if (!plan) + return; + + if (plan.value().updates_selection) { auto button = static_cast(target); button->set_active(true); @@ -281,7 +319,9 @@ void NodePanelQuick::handle_button_color_click(Node* target) return; } - // if the box is already selected show the popup + if (!plan.value().opens_color_picker) + return; + auto popup = m_picker; auto screen = root()->m_size; glm::vec2 tick_sz = { 16, 32 }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f17a897..978f892 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -288,6 +288,16 @@ add_test(NAME pp_app_core_grid_ui_tests COMMAND pp_app_core_grid_ui_tests) set_tests_properties(pp_app_core_grid_ui_tests PROPERTIES LABELS "app;ui;renderer;desktop-fast;fuzz") +add_executable(pp_app_core_quick_ui_tests + app_core/quick_ui_tests.cpp) +target_link_libraries(pp_app_core_quick_ui_tests PRIVATE + pp_app_core + pp_test_harness) + +add_test(NAME pp_app_core_quick_ui_tests COMMAND pp_app_core_quick_ui_tests) +set_tests_properties(pp_app_core_quick_ui_tests PROPERTIES + LABELS "app;ui;paint;desktop-fast;fuzz") + add_executable(pp_app_core_document_route_tests app_core/document_route_tests.cpp) target_link_libraries(pp_app_core_document_route_tests PRIVATE @@ -862,6 +872,42 @@ if(TARGET pano_cli) LABELS "app;ui;renderer;integration;desktop-fast;fuzz" WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_quick_operation_select_brush_smoke + COMMAND pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 2) + set_tests_properties(pano_cli_plan_quick_operation_select_brush_smoke PROPERTIES + LABELS "app;ui;paint;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-quick-operation\".*\"operation\":\"select-slot\".*\"slotKind\":\"brush\".*\"slotIndex\":2.*\"updatesSelection\":true.*\"invokesChangeCallback\":true") + + add_test(NAME pano_cli_plan_quick_operation_open_color_smoke + COMMAND pano_cli plan-quick-operation --kind color --current-index 1 --slot-index 1) + set_tests_properties(pano_cli_plan_quick_operation_open_color_smoke PROPERTIES + LABELS "app;ui;paint;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-quick-operation\".*\"operation\":\"open-slot-popup\".*\"slotKind\":\"color\".*\"opensColorPicker\":true.*\"mutatesQuickState\":false") + + add_test(NAME pano_cli_plan_quick_operation_restore_smoke + COMMAND pano_cli plan-quick-operation --kind restore --brush-index 2 --color-index 1 --fire-event) + set_tests_properties(pano_cli_plan_quick_operation_restore_smoke PROPERTIES + LABELS "app;ui;paint;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-quick-operation\".*\"operation\":\"restore-state\".*\"fireEvent\":true.*\"invokesChangeCallback\":true.*\"restoresSlots\":true.*\"redrawsBrushPreviews\":true") + + add_test(NAME pano_cli_plan_quick_operation_reset_smoke + COMMAND pano_cli plan-quick-operation --kind reset) + set_tests_properties(pano_cli_plan_quick_operation_reset_smoke PROPERTIES + LABELS "app;ui;paint;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-quick-operation\".*\"operation\":\"reset-state\".*\"invokesChangeCallback\":false.*\"resetsSlots\":true.*\"redrawsBrushPreviews\":true") + + add_test(NAME pano_cli_plan_quick_operation_rejects_bad_slot + COMMAND pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 3) + set_tests_properties(pano_cli_plan_quick_operation_rejects_bad_slot PROPERTIES + LABELS "app;ui;paint;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + + add_test(NAME pano_cli_plan_quick_operation_rejects_bad_restore + COMMAND pano_cli plan-quick-operation --kind restore --brush-index -1 --color-index 0) + set_tests_properties(pano_cli_plan_quick_operation_rejects_bad_restore PROPERTIES + LABELS "app;ui;paint;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_share_file_unsaved_smoke COMMAND pano_cli plan-share-file) set_tests_properties(pano_cli_plan_share_file_unsaved_smoke PROPERTIES diff --git a/tests/app_core/quick_ui_tests.cpp b/tests/app_core/quick_ui_tests.cpp new file mode 100644 index 0000000..720b211 --- /dev/null +++ b/tests/app_core/quick_ui_tests.cpp @@ -0,0 +1,83 @@ +#include "app_core/quick_ui.h" +#include "test_harness.h" + +namespace { + +void slot_click_selects_or_opens_popup(pp::tests::Harness& harness) +{ + const auto select_brush = pp::app::plan_quick_slot_click(pp::app::QuickUiSlotKind::brush, 0, 2, 3); + PP_EXPECT(harness, select_brush); + if (select_brush) { + PP_EXPECT(harness, select_brush.value().operation == pp::app::QuickUiOperation::select_slot); + PP_EXPECT(harness, select_brush.value().slot_kind == pp::app::QuickUiSlotKind::brush); + PP_EXPECT(harness, select_brush.value().slot_index == 2); + PP_EXPECT(harness, select_brush.value().previous_index == 0); + PP_EXPECT(harness, select_brush.value().updates_selection); + PP_EXPECT(harness, select_brush.value().invokes_change_callback); + PP_EXPECT(harness, select_brush.value().mutates_quick_state); + PP_EXPECT(harness, !select_brush.value().opens_brush_popup); + } + + const auto open_brush = pp::app::plan_quick_slot_click(pp::app::QuickUiSlotKind::brush, 1, 1, 3); + PP_EXPECT(harness, open_brush); + if (open_brush) { + PP_EXPECT(harness, open_brush.value().operation == pp::app::QuickUiOperation::open_slot_popup); + PP_EXPECT(harness, open_brush.value().opens_brush_popup); + PP_EXPECT(harness, !open_brush.value().opens_color_picker); + PP_EXPECT(harness, !open_brush.value().updates_selection); + PP_EXPECT(harness, !open_brush.value().mutates_quick_state); + } + + const auto open_color = pp::app::plan_quick_slot_click(pp::app::QuickUiSlotKind::color, 0, 0, 3); + PP_EXPECT(harness, open_color); + if (open_color) { + PP_EXPECT(harness, open_color.value().opens_color_picker); + PP_EXPECT(harness, !open_color.value().opens_brush_popup); + } +} + +void slot_click_rejects_invalid_indices(pp::tests::Harness& harness) +{ + PP_EXPECT(harness, !pp::app::plan_quick_slot_click(pp::app::QuickUiSlotKind::brush, -1, 0, 3)); + PP_EXPECT(harness, !pp::app::plan_quick_slot_click(pp::app::QuickUiSlotKind::brush, 0, 3, 3)); + PP_EXPECT(harness, !pp::app::plan_quick_slot_click(pp::app::QuickUiSlotKind::color, 0, 0, 0)); +} + +void restore_and_reset_validate_state(pp::tests::Harness& harness) +{ + const auto restore = pp::app::plan_quick_state_restore(2, 1, 3, true); + PP_EXPECT(harness, restore); + if (restore) { + PP_EXPECT(harness, restore.value().operation == pp::app::QuickUiOperation::restore_state); + PP_EXPECT(harness, restore.value().slot_count == 3); + PP_EXPECT(harness, restore.value().fire_event); + PP_EXPECT(harness, restore.value().restores_slots); + PP_EXPECT(harness, restore.value().redraws_brush_previews); + PP_EXPECT(harness, restore.value().invokes_change_callback); + } + + const auto reset = pp::app::plan_quick_state_reset(3, false); + PP_EXPECT(harness, reset); + if (reset) { + PP_EXPECT(harness, reset.value().operation == pp::app::QuickUiOperation::reset_state); + PP_EXPECT(harness, reset.value().resets_slots); + PP_EXPECT(harness, reset.value().redraws_brush_previews); + PP_EXPECT(harness, !reset.value().invokes_change_callback); + PP_EXPECT(harness, reset.value().mutates_quick_state); + } + + PP_EXPECT(harness, !pp::app::plan_quick_state_restore(3, 0, 3, false)); + PP_EXPECT(harness, !pp::app::plan_quick_state_restore(0, -1, 3, false)); + PP_EXPECT(harness, !pp::app::plan_quick_state_reset(0, false)); +} + +} // namespace + +int main() +{ + pp::tests::Harness harness; + harness.run("slot click selects or opens popup", slot_click_selects_or_opens_popup); + harness.run("slot click rejects invalid indices", slot_click_rejects_invalid_indices); + harness.run("restore and reset validate state", restore_and_reset_validate_state); + return harness.finish(); +} diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 3ecf4e4..948ecdc 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -12,6 +12,7 @@ #include "app_core/document_sharing.h" #include "app_core/document_session.h" #include "app_core/grid_ui.h" +#include "app_core/quick_ui.h" #include "assets/image_format.h" #include "assets/image_metadata.h" #include "assets/image_pixels.h" @@ -275,6 +276,16 @@ struct PlanGridOperationArgs { int sample_count = 32; }; +struct PlanQuickOperationArgs { + std::string kind = "brush"; + int current_index = 0; + int slot_index = 0; + int brush_index = 0; + int color_index = 0; + int slot_count = 3; + bool fire_event = false; +}; + struct SimulateAppSessionArgs { bool has_canvas = true; bool new_document = false; @@ -624,6 +635,34 @@ const char* grid_ui_operation_name(pp::app::GridUiOperation operation) noexcept return "request-heightmap-pick"; } +const char* quick_ui_slot_kind_name(pp::app::QuickUiSlotKind kind) noexcept +{ + switch (kind) { + case pp::app::QuickUiSlotKind::brush: + return "brush"; + case pp::app::QuickUiSlotKind::color: + return "color"; + } + + return "brush"; +} + +const char* quick_ui_operation_name(pp::app::QuickUiOperation operation) noexcept +{ + switch (operation) { + case pp::app::QuickUiOperation::select_slot: + return "select-slot"; + case pp::app::QuickUiOperation::open_slot_popup: + return "open-slot-popup"; + case pp::app::QuickUiOperation::restore_state: + return "restore-state"; + case pp::app::QuickUiOperation::reset_state: + return "reset-state"; + } + + return "select-slot"; +} + const char* document_file_write_decision_name(pp::app::DocumentFileWriteDecision decision) noexcept { switch (decision) { @@ -880,6 +919,7 @@ void print_help() << " plan-animation-operation --kind add|duplicate|remove|duration|move|goto|next|prev|onion [--frame-count N] [--total-duration N] [--current-frame N] [--selected-frame N] [--current-duration N] [--delta N] [--offset N] [--onion-size N]\n" << " plan-brush-operation --kind color|tip|pattern|dual|preset|settings [--path FILE] [--thumb FILE] [--r N] [--g N] [--b N] [--a N] [--no-brush]\n" << " plan-grid-operation --kind pick|load|reload|clear|render|commit [--path FILE] [--no-heightmap] [--no-canvas] [--float32] [--float16] [--texture-resolution N] [--samples N]\n" + << " plan-quick-operation --kind brush|color|restore|reset [--current-index N] [--slot-index N] [--brush-index N] [--color-index N] [--slot-count N] [--fire-event]\n" << " plan-share-file [--path FILE]\n" << " plan-picked-path [--path FILE]\n" << " plan-display-file [--path FILE]\n" @@ -3039,6 +3079,122 @@ int plan_grid_operation(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_quick_operation_args( + int argc, + char** argv, + PlanQuickOperationArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--kind") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + args.kind = argv[++i]; + } else if (key == "--current-index" || key == "--slot-index" || key == "--brush-index" + || key == "--color-index" || key == "--slot-count") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = parse_i32_arg(argv[++i]); + if (!value) { + return value.status(); + } + if (key == "--current-index") { + args.current_index = value.value(); + } else if (key == "--slot-index") { + args.slot_index = value.value(); + } else if (key == "--brush-index") { + args.brush_index = value.value(); + } else if (key == "--color-index") { + args.color_index = value.value(); + } else { + args.slot_count = value.value(); + } + } else if (key == "--fire-event") { + args.fire_event = true; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +pp::foundation::Result make_quick_operation_plan( + const PlanQuickOperationArgs& args) +{ + if (args.kind == "brush") { + return pp::app::plan_quick_slot_click( + pp::app::QuickUiSlotKind::brush, + args.current_index, + args.slot_index, + args.slot_count); + } + if (args.kind == "color") { + return pp::app::plan_quick_slot_click( + pp::app::QuickUiSlotKind::color, + args.current_index, + args.slot_index, + args.slot_count); + } + if (args.kind == "restore") { + return pp::app::plan_quick_state_restore( + args.brush_index, + args.color_index, + args.slot_count, + args.fire_event); + } + if (args.kind == "reset") { + return pp::app::plan_quick_state_reset(args.slot_count, args.fire_event); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown quick operation kind")); +} + +int plan_quick_operation(int argc, char** argv) +{ + PlanQuickOperationArgs args; + const auto status = parse_plan_quick_operation_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-quick-operation", status.message); + return 2; + } + + const auto plan = make_quick_operation_plan(args); + if (!plan) { + print_error("plan-quick-operation", plan.status().message); + return 2; + } + + const auto& value = plan.value(); + std::cout << "{\"ok\":true,\"command\":\"plan-quick-operation\"" + << ",\"state\":{\"kind\":\"" << json_escape(args.kind) + << "\",\"currentIndex\":" << args.current_index + << ",\"slotIndex\":" << args.slot_index + << ",\"brushIndex\":" << args.brush_index + << ",\"colorIndex\":" << args.color_index + << ",\"slotCount\":" << args.slot_count + << ",\"fireEvent\":" << json_bool(args.fire_event) + << "},\"plan\":{\"operation\":\"" << quick_ui_operation_name(value.operation) + << "\",\"slotKind\":\"" << quick_ui_slot_kind_name(value.slot_kind) + << "\",\"slotIndex\":" << value.slot_index + << ",\"previousIndex\":" << value.previous_index + << ",\"slotCount\":" << value.slot_count + << ",\"fireEvent\":" << json_bool(value.fire_event) + << ",\"updatesSelection\":" << json_bool(value.updates_selection) + << ",\"opensBrushPopup\":" << json_bool(value.opens_brush_popup) + << ",\"opensColorPicker\":" << json_bool(value.opens_color_picker) + << ",\"invokesChangeCallback\":" << json_bool(value.invokes_change_callback) + << ",\"restoresSlots\":" << json_bool(value.restores_slots) + << ",\"resetsSlots\":" << json_bool(value.resets_slots) + << ",\"redrawsBrushPreviews\":" << json_bool(value.redraws_brush_previews) + << ",\"mutatesQuickState\":" << json_bool(value.mutates_quick_state) + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_share_file_args( int argc, char** argv, @@ -5463,6 +5619,10 @@ int main(int argc, char** argv) return plan_grid_operation(argc, argv); } + if (command == "plan-quick-operation") { + return plan_quick_operation(argc, argv); + } + if (command == "plan-share-file") { return plan_share_file(argc, argv); }