Extract quick UI operation planning

This commit is contained in:
2026-06-03 11:01:01 +02:00
parent 73fac0f8e4
commit 8dc476d205
8 changed files with 499 additions and 11 deletions

View File

@@ -236,7 +236,8 @@ add_library(pp_app_core STATIC
src/app_core/document_route.cpp src/app_core/document_route.cpp
src/app_core/document_sharing.h src/app_core/document_sharing.h
src/app_core/document_session.cpp 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 target_include_directories(pp_app_core
PUBLIC PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src") "${CMAKE_CURRENT_SOURCE_DIR}/src")

View File

@@ -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-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-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-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 ## Closed Debt

View File

@@ -503,6 +503,10 @@ callbacks before legacy `Brush` mutation and resource loading continue.
pick/load/reload/clear, lightmap render capability/limit checks, and heightmap pick/load/reload/clear, lightmap render capability/limit checks, and heightmap
commit used by the live grid panel before legacy image loading, OpenGL texture commit used by the live grid panel before legacy image loading, OpenGL texture
updates, nanort lightmap baking, and `Canvas::draw_objects` execution continue. 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 `pp_platform_api` now owns a headless `PlatformServices` interface for
startup storage path preparation, clipboard text, cursor visibility, startup storage path preparation, clipboard text, cursor visibility,
virtual-keyboard visibility, UI-thread lifecycle hooks, render-context 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_empty_reload`, and
`pano_cli_plan_grid_operation_rejects_bad_samples` passed and expose live `pano_cli_plan_grid_operation_rejects_bad_samples` passed and expose live
grid/heightmap/lightmap planning as JSON automation. 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 - `pp_app_core_document_sharing_tests` passed, covering saved-path gating before
platform share execution. platform share execution.
- `pano_cli_plan_share_file_unsaved_smoke` and - `pano_cli_plan_share_file_unsaved_smoke` and

143
src/app_core/quick_ui.h Normal file
View File

@@ -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<QuickUiPlan> 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<QuickUiPlan>::failure(current_status);
}
const auto clicked_status = validate_quick_slot_index(clicked_index, slot_count);
if (!clicked_status.ok()) {
return pp::foundation::Result<QuickUiPlan>::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<QuickUiPlan>::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<QuickUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> 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<QuickUiPlan>::failure(brush_status);
}
const auto color_status = validate_quick_slot_index(color_index, slot_count);
if (!color_status.ok()) {
return pp::foundation::Result<QuickUiPlan>::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<QuickUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> 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<QuickUiPlan>::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<QuickUiPlan>::success(plan);
}
} // namespace pp::app

View File

@@ -1,4 +1,5 @@
#include "pch.h" #include "pch.h"
#include "app_core/quick_ui.h"
#include "node_panel_quick.h" #include "node_panel_quick.h"
#include "node_stroke_preview.h" #include "node_stroke_preview.h"
#include "node_image.h" #include "node_image.h"
@@ -31,11 +32,13 @@ void NodePanelQuick::set_color(glm::vec3 color)
int NodePanelQuick::get_selected_brush_index() const int NodePanelQuick::get_selected_brush_index() const
{ {
auto it = std::find(m_button_brushes.begin(), m_button_brushes.end(), m_button_brush_current); 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<int>(std::distance(m_button_brushes.begin(), it));
} }
void NodePanelQuick::set_selected_brush_index(int idx, bool fire_event /*= false*/) void NodePanelQuick::set_selected_brush_index(int idx, bool fire_event /*= false*/)
{ {
if (!pp::app::validate_quick_slot_index(idx, static_cast<int>(m_button_brushes.size())).ok())
return;
if (m_button_brush_current) if (m_button_brush_current)
m_button_brush_current->set_active(false); m_button_brush_current->set_active(false);
m_button_brush_current = m_button_brushes[idx]; 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 int NodePanelQuick::get_selected_color_index() const
{ {
auto it = std::find(m_button_colors.begin(), m_button_colors.end(), m_button_color_current); 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<int>(std::distance(m_button_colors.begin(), it));
} }
void NodePanelQuick::set_selected_color_index(int idx, bool fire_event /*= false*/) void NodePanelQuick::set_selected_color_index(int idx, bool fire_event /*= false*/)
{ {
if (!pp::app::validate_quick_slot_index(idx, static_cast<int>(m_button_colors.size())).ok())
return;
if (m_button_color_current) if (m_button_color_current)
m_button_color_current->set_active(false); m_button_color_current->set_active(false);
m_button_color_current = m_button_colors[idx]; 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*/) 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<int>(m_button_brushes.size()),
fire_event);
if (!plan)
return;
for (int i = 0; i < 3; i++) for (int i = 0; i < 3; i++)
{ {
auto b = static_cast<NodeStrokePreview*>(m_button_brushes[i]->m_children[0].get()); auto b = static_cast<NodeStrokePreview*>(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*/) void NodePanelQuick::reset_state(bool fire_event /*= false*/)
{ {
const auto plan = pp::app::plan_quick_state_reset(static_cast<int>(m_button_brushes.size()), fire_event);
if (!plan)
return;
for (int i = 0; i < 3; i++) for (int i = 0; i < 3; i++)
{ {
auto b = static_cast<NodeStrokePreview*>(m_button_brushes[i]->m_children[0].get()); auto b = static_cast<NodeStrokePreview*>(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()); LOG("init_button_brush %s", name.c_str());
auto button = find<NodeButtonCustom>(name.c_str()); auto button = find<NodeButtonCustom>(name.c_str());
if (!button) if (!button) {
LOG("couldn't find button %s", name.c_str()); 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); 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<int>(button->m_children.size()));
auto pr = static_cast<NodeStrokePreview*>(button->m_children[0].get()); auto pr = static_cast<NodeStrokePreview*>(button->m_children[0].get());
pr->m_brush = std::make_shared<Brush>(); pr->m_brush = std::make_shared<Brush>();
pr->m_brush->m_tip_size_pressure = szp; 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) void NodePanelQuick::handle_button_brush_click(Node* button)
{ {
// the first time select the box const auto clicked = std::find(m_button_brushes.begin(), m_button_brushes.end(), button);
if (m_button_brush_current != button) const int clicked_index = static_cast<int>(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<int>(m_button_brushes.size()));
if (!plan)
return;
if (plan.value().updates_selection)
{ {
auto b = static_cast<NodeButtonCustom*>(button); auto b = static_cast<NodeButtonCustom*>(button);
b->set_active(true); b->set_active(true);
@@ -210,7 +238,8 @@ void NodePanelQuick::handle_button_brush_click(Node* button)
return; return;
} }
// if the box is already selected show the popup if (!plan.value().opens_brush_popup)
return;
auto popup = App::I->presets; auto popup = App::I->presets;
auto screen = root()->m_size; 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) void NodePanelQuick::handle_button_color_click(Node* target)
{ {
// the first time select the box const auto clicked = std::find(m_button_colors.begin(), m_button_colors.end(), target);
if (m_button_color_current != target) const int clicked_index = static_cast<int>(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<int>(m_button_colors.size()));
if (!plan)
return;
if (plan.value().updates_selection)
{ {
auto button = static_cast<NodeButtonCustom*>(target); auto button = static_cast<NodeButtonCustom*>(target);
button->set_active(true); button->set_active(true);
@@ -281,7 +319,9 @@ void NodePanelQuick::handle_button_color_click(Node* target)
return; return;
} }
// if the box is already selected show the popup if (!plan.value().opens_color_picker)
return;
auto popup = m_picker; auto popup = m_picker;
auto screen = root()->m_size; auto screen = root()->m_size;
glm::vec2 tick_sz = { 16, 32 }; glm::vec2 tick_sz = { 16, 32 };

View File

@@ -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 set_tests_properties(pp_app_core_grid_ui_tests PROPERTIES
LABELS "app;ui;renderer;desktop-fast;fuzz") 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 add_executable(pp_app_core_document_route_tests
app_core/document_route_tests.cpp) app_core/document_route_tests.cpp)
target_link_libraries(pp_app_core_document_route_tests PRIVATE 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" LABELS "app;ui;renderer;integration;desktop-fast;fuzz"
WILL_FAIL TRUE) 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 add_test(NAME pano_cli_plan_share_file_unsaved_smoke
COMMAND pano_cli plan-share-file) COMMAND pano_cli plan-share-file)
set_tests_properties(pano_cli_plan_share_file_unsaved_smoke PROPERTIES set_tests_properties(pano_cli_plan_share_file_unsaved_smoke PROPERTIES

View File

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

View File

@@ -12,6 +12,7 @@
#include "app_core/document_sharing.h" #include "app_core/document_sharing.h"
#include "app_core/document_session.h" #include "app_core/document_session.h"
#include "app_core/grid_ui.h" #include "app_core/grid_ui.h"
#include "app_core/quick_ui.h"
#include "assets/image_format.h" #include "assets/image_format.h"
#include "assets/image_metadata.h" #include "assets/image_metadata.h"
#include "assets/image_pixels.h" #include "assets/image_pixels.h"
@@ -275,6 +276,16 @@ struct PlanGridOperationArgs {
int sample_count = 32; 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 { struct SimulateAppSessionArgs {
bool has_canvas = true; bool has_canvas = true;
bool new_document = false; bool new_document = false;
@@ -624,6 +635,34 @@ const char* grid_ui_operation_name(pp::app::GridUiOperation operation) noexcept
return "request-heightmap-pick"; 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 const char* document_file_write_decision_name(pp::app::DocumentFileWriteDecision decision) noexcept
{ {
switch (decision) { 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-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-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-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-share-file [--path FILE]\n"
<< " plan-picked-path [--path FILE]\n" << " plan-picked-path [--path FILE]\n"
<< " plan-display-file [--path FILE]\n" << " plan-display-file [--path FILE]\n"
@@ -3039,6 +3079,122 @@ int plan_grid_operation(int argc, char** argv)
return 0; 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<pp::app::QuickUiPlan> 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<pp::app::QuickUiPlan>::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( pp::foundation::Status parse_plan_share_file_args(
int argc, int argc,
char** argv, char** argv,
@@ -5463,6 +5619,10 @@ int main(int argc, char** argv)
return plan_grid_operation(argc, argv); return plan_grid_operation(argc, argv);
} }
if (command == "plan-quick-operation") {
return plan_quick_operation(argc, argv);
}
if (command == "plan-share-file") { if (command == "plan-share-file") {
return plan_share_file(argc, argv); return plan_share_file(argc, argv);
} }