diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 503cc7b..1be9e96 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -42,7 +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/stroke-settings UI planning and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, and the `BrushUiServices` boundary, but the live adapter still mutates legacy `Brush`, calls legacy brush texture loading, and refreshes legacy quick/stroke/color widgets | 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/stroke-settings execution is owned by injected brush/app/asset/UI services with no legacy brush 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 | +| DEBT-0025 | Open | Modernization | Quick brush/color slot and mini-state planning and execution dispatch now consume pure `pp_app_core` through `NodePanelQuick`, `pano_cli plan-quick-operation`, and the `QuickUiServices` boundary, but the live adapter still mutates legacy quick UI widgets, `Brush` previews, color picker popup state, and preset popup state | 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 injected app/brush/UI services with no legacy quick-panel adapter | | DEBT-0026 | Open | Modernization | Toolbar history command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, `pano_cli plan-history-operation`, and the `HistoryUiServices` boundary, but the live adapter still mutates legacy `ActionManager` stacks directly | Preserve undo/redo/clear behavior while moving action history toward document/app command services | `pp_app_core_history_ui_tests`; `pano_cli plan-history-operation --kind undo --undo-count 2`; `pano_cli plan-history-operation --kind clear --undo-count 2 --redo-count 1 --memory-bytes 4096`; `ctest --preset desktop-fast --build-config Debug` | Undo/redo/clear execution is owned by injected document/app history services with no legacy `ActionManager` adapter | | DEBT-0027 | Open | Modernization | Canvas draw-tool toolbar command, canvas input mode switching, and active-state planning now consume pure `pp_app_core` through `App::init_toolbar_draw`, `App::update`, `NodeCanvas`, `pano_cli plan-canvas-tool`, and `pano_cli plan-canvas-tool-state`, but live execution/state storage still mutates or reads legacy `Canvas` mode state, pen picking state, touch-lock state, and transform copy/cut action objects directly | Preserve current toolbar, stylus eraser, and keyboard draw/erase behavior while canvas input/tools move toward an app/document command boundary | `pp_app_core_canvas_tool_ui_tests`; `pano_cli plan-canvas-tool --kind copy`; `pano_cli plan-canvas-tool-state --mode draw --picking --touch-lock`; `ctest --preset desktop-fast --build-config Debug` | Canvas tool selection, toolbar state refresh, picking, touch lock, stylus eraser/key mode switching, and transform action execution are owned by app/document/canvas services with toolbar/canvas callbacks acting only as adapters | | DEBT-0028 | Open | Modernization | Canvas clear command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-canvas-clear`, and the `DocumentCanvasClearServices` boundary, but the live adapter still calls legacy `Canvas::clear`, which records `ActionLayerClear`, clears the current layer/frame, and marks legacy `Canvas::I` unsaved | Preserve clear-current-layer behavior while canvas/document commands move toward document/app command services | `pp_app_core_document_canvas_tests`; `pano_cli plan-canvas-clear --r 0 --g 0.1 --b 0.2 --a 0.3`; `pano_cli plan-canvas-clear --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Canvas clear execution, undo recording, dirty-state updates, and clear color handling are owned by injected document/app services with no legacy canvas-clear adapter | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 22368a2..9ce41f1 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -551,8 +551,9 @@ through a legacy adapter before legacy dialogs, history/canvas adapters, and settings UI 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. +validation used by the live quick panel. Quick-panel execution now dispatches +through `QuickUiServices` before the legacy `Brush`, color picker, stroke +preview, and preset popup adapter continues. `pano_cli plan-tools-menu` and `pano_cli plan-tools-panel` expose app-core planning for top-level Tools commands and floating-panel requests, including already-visible no-ops, panel chrome metadata, shortcuts, camera reset, @@ -1285,7 +1286,8 @@ Results: toolbar/canvas history 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. + validation, reset-state validation, service dispatch order, explicit + brush/color restore indices, and malformed execution payload rejection. - `pano_cli_plan_quick_operation_select_brush_smoke`, `pano_cli_plan_quick_operation_open_color_smoke`, `pano_cli_plan_quick_operation_restore_smoke`, diff --git a/src/app_core/quick_ui.h b/src/app_core/quick_ui.h index 6d6d598..0e7ee71 100644 --- a/src/app_core/quick_ui.h +++ b/src/app_core/quick_ui.h @@ -21,6 +21,8 @@ struct QuickUiPlan { QuickUiSlotKind slot_kind = QuickUiSlotKind::brush; int slot_index = 0; int previous_index = 0; + int brush_index = 0; + int color_index = 0; int slot_count = 0; bool fire_event = false; bool updates_selection = false; @@ -33,6 +35,16 @@ struct QuickUiPlan { bool mutates_quick_state = false; }; +class QuickUiServices { +public: + virtual ~QuickUiServices() = default; + + virtual void select_slot(QuickUiSlotKind slot_kind, int slot_index, bool fire_event) = 0; + virtual void open_slot_popup(QuickUiSlotKind slot_kind, int slot_index) = 0; + virtual void restore_state(int brush_index, int color_index, bool fire_event) = 0; + virtual void reset_state(bool fire_event) = 0; +}; + [[nodiscard]] inline pp::foundation::Status validate_quick_slot_count(int slot_count) noexcept { if (slot_count <= 0) { @@ -76,6 +88,8 @@ struct QuickUiPlan { plan.slot_kind = slot_kind; plan.slot_index = clicked_index; plan.previous_index = current_index; + plan.brush_index = slot_kind == QuickUiSlotKind::brush ? clicked_index : 0; + plan.color_index = slot_kind == QuickUiSlotKind::color ? clicked_index : 0; plan.slot_count = slot_count; if (clicked_index != current_index) { plan.operation = QuickUiOperation::select_slot; @@ -109,6 +123,8 @@ struct QuickUiPlan { QuickUiPlan plan; plan.operation = QuickUiOperation::restore_state; + plan.brush_index = brush_index; + plan.color_index = color_index; plan.slot_count = slot_count; plan.fire_event = fire_event; plan.updates_selection = true; @@ -130,6 +146,8 @@ struct QuickUiPlan { QuickUiPlan plan; plan.operation = QuickUiOperation::reset_state; + plan.brush_index = 0; + plan.color_index = 0; plan.slot_count = slot_count; plan.fire_event = fire_event; plan.updates_selection = true; @@ -140,4 +158,72 @@ struct QuickUiPlan { return pp::foundation::Result::success(plan); } +[[nodiscard]] inline pp::foundation::Status execute_quick_ui_plan( + const QuickUiPlan& plan, + QuickUiServices& services) +{ + switch (plan.operation) { + case QuickUiOperation::select_slot: + if (!plan.updates_selection) { + return pp::foundation::Status::invalid_argument("quick select plan must update selection"); + } + { + const auto status = validate_quick_slot_index(plan.slot_index, plan.slot_count); + if (!status.ok()) { + return status; + } + } + services.select_slot(plan.slot_kind, plan.slot_index, plan.invokes_change_callback); + return pp::foundation::Status::success(); + + case QuickUiOperation::open_slot_popup: + if (plan.slot_kind == QuickUiSlotKind::brush && !plan.opens_brush_popup) { + return pp::foundation::Status::invalid_argument("quick brush popup plan must open brush popup"); + } + if (plan.slot_kind == QuickUiSlotKind::color && !plan.opens_color_picker) { + return pp::foundation::Status::invalid_argument("quick color popup plan must open color picker"); + } + { + const auto status = validate_quick_slot_index(plan.slot_index, plan.slot_count); + if (!status.ok()) { + return status; + } + } + services.open_slot_popup(plan.slot_kind, plan.slot_index); + return pp::foundation::Status::success(); + + case QuickUiOperation::restore_state: + if (!plan.restores_slots) { + return pp::foundation::Status::invalid_argument("quick restore plan must restore slots"); + } + { + const auto brush_status = validate_quick_slot_index(plan.brush_index, plan.slot_count); + if (!brush_status.ok()) { + return brush_status; + } + const auto color_status = validate_quick_slot_index(plan.color_index, plan.slot_count); + if (!color_status.ok()) { + return color_status; + } + } + services.restore_state(plan.brush_index, plan.color_index, plan.fire_event); + return pp::foundation::Status::success(); + + case QuickUiOperation::reset_state: + if (!plan.resets_slots) { + return pp::foundation::Status::invalid_argument("quick reset plan must reset slots"); + } + { + const auto status = validate_quick_slot_count(plan.slot_count); + if (!status.ok()) { + return status; + } + } + services.reset_state(plan.fire_event); + return pp::foundation::Status::success(); + } + + return pp::foundation::Status::invalid_argument("unknown quick UI operation"); +} + } // namespace pp::app diff --git a/src/node_panel_quick.cpp b/src/node_panel_quick.cpp index 300a6fb..62c270a 100644 --- a/src/node_panel_quick.cpp +++ b/src/node_panel_quick.cpp @@ -5,6 +5,194 @@ #include "node_image.h" #include "app.h" +namespace { + +class LegacyQuickUiServices final : public pp::app::QuickUiServices { +public: + LegacyQuickUiServices(NodePanelQuick& panel, const NodePanelQuick::MiniState* restore_state = nullptr) noexcept + : panel_(panel) + , restore_state_(restore_state) + { + } + + void select_slot(pp::app::QuickUiSlotKind slot_kind, int slot_index, bool fire_event) override + { + if (slot_kind == pp::app::QuickUiSlotKind::brush) { + panel_.set_selected_brush_index(slot_index, fire_event); + return; + } + + panel_.set_selected_color_index(slot_index, fire_event); + } + + void open_slot_popup(pp::app::QuickUiSlotKind slot_kind, int slot_index) override + { + if (slot_kind == pp::app::QuickUiSlotKind::brush) { + open_brush_popup(slot_index); + return; + } + + open_color_picker(slot_index); + } + + void restore_state(int brush_index, int color_index, bool fire_event) override + { + if (!restore_state_) + return; + + for (int i = 0; i < static_cast(panel_.m_button_brushes.size()); i++) + { + auto b = static_cast(panel_.m_button_brushes[i]->m_children[0].get()); + b->m_brush = restore_state_->brushes[i]; + b->draw_stroke(); + auto c = static_cast(panel_.m_button_colors[i]->m_children[0].get()); + c->m_color = restore_state_->colors[i]; + } + panel_.set_selected_color_index(color_index, fire_event); + panel_.set_selected_brush_index(brush_index, fire_event); + } + + void reset_state(bool fire_event) override + { + for (int i = 0; i < static_cast(panel_.m_button_brushes.size()); i++) + { + auto b = static_cast(panel_.m_button_brushes[i]->m_children[0].get()); + b->m_brush = std::make_shared(); + b->m_brush->load_tip("data/brushes/Round-Hard.png", "data/brushes/thumbs/Round-Hard.png"); + b->draw_stroke(); + } + static_cast(panel_.m_button_colors[0]->m_children[0].get())->m_color = glm::vec4(0, 0, 0, 1); + static_cast(panel_.m_button_colors[1]->m_children[0].get())->m_color = glm::vec4(.5, .5, .5, 1); + static_cast(panel_.m_button_colors[2]->m_children[0].get())->m_color = glm::vec4(1, 1, 1, 1); + panel_.set_selected_brush_index(0, fire_event); + panel_.set_selected_color_index(0, fire_event); + } + +private: + void open_brush_popup(int slot_index) + { + auto button = panel_.m_button_brushes[slot_index]; + if (!button) + return; + + auto popup = App::I->presets; + auto screen = panel_.root()->m_size; + glm::vec2 tick_sz = { 16, 32 }; + glm::vec2 tick_pos = button->m_pos + glm::vec2(button->m_size.x, 0); + glm::vec2 popup_pos = { tick_pos.x + tick_sz.x, tick_pos.y }; + + auto tick = panel_.root()->add_child(); + tick->SetPositioning(YGPositionTypeAbsolute); + tick->SetPosition(tick_pos.x, tick_pos.y + (button->m_size.y - tick_sz.y) * 0.5f); + tick->SetSize(tick_sz); + tick->set_image("data/ui/popup-tick.png"); + tick->m_scale = { 1, 1 }; + + float hh = popup->m_container->m_children.size() > 10 ? (screen.y - 90.f) : 400.f; + popup->SetWidth(350); + popup->SetHeight(glm::max(hh, 400.f)); + popup->SetPositioning(YGPositionTypeAbsolute); + popup->SetPosition(popup_pos); + panel_.root()->add_child(popup); + + panel_.root()->update(); + popup->tick(0); + popup->update(); + + if (tick_pos.x + popup->m_size.x > screen.x) + { + tick_pos = button->m_pos - glm::vec2(tick_sz.x, 0); + popup_pos = { tick_pos.x - popup->GetWidth(), tick_pos.y }; + tick->m_scale.x = -1.f; + } + popup_pos = glm::clamp(popup_pos, { 0, 0 }, screen - popup->m_size); + popup->SetPosition(popup_pos); + tick->SetPosition(tick_pos.x, tick_pos.y + (button->m_size.y - tick_sz.y) * 0.5f); + popup->update(); + + popup->m_mouse_ignore = false; + popup->m_flood_events = true; + popup->m_capture_children = false; + popup->mouse_capture(); + + popup->on_popup_close = [tick](Node*) { + tick->destroy(); + }; + + auto* panel = &panel_; + popup->on_brush_changed = [panel, button](Node*, std::shared_ptr& b) { + auto pr = static_cast(button->m_children[0].get()); + *pr->m_brush = *b; + pr->m_brush->load(); + pr->draw_stroke(); + if (panel->on_brush_change) + panel->on_brush_change(button, pr->m_brush); + }; + } + + void open_color_picker(int slot_index) + { + auto target = panel_.m_button_colors[slot_index]; + if (!target) + return; + + auto popup = panel_.m_picker; + auto screen = panel_.root()->m_size; + glm::vec2 tick_sz = { 16, 32 }; + glm::vec2 tick_pos = target->m_pos + glm::vec2(target->m_size.x, 0); + glm::vec2 popup_pos = { tick_pos.x + tick_sz.x, tick_pos.y - 140.f }; + + auto tick = panel_.root()->add_child(); + tick->SetPositioning(YGPositionTypeAbsolute); + tick->SetPosition(tick_pos.x, tick_pos.y + (target->m_size.y - tick_sz.y) * 0.5f); + tick->SetSize(tick_sz); + tick->set_image("data/ui/popup-tick.png"); + tick->m_scale = { 1, 1 }; + + popup->SetPositioning(YGPositionTypeAbsolute); + popup->SetPosition(popup_pos); + panel_.root()->add_child(popup); + + panel_.root()->update(); + popup->tick(0); + popup->update(); + + if (tick_pos.x + popup->m_size.x > screen.x) + { + tick_pos = target->m_pos - glm::vec2(tick_sz.x, 0); + popup_pos = { tick_pos.x - popup->GetWidth(), tick_pos.y - 140.f }; + tick->m_scale.x = -1.f; + } + popup_pos = glm::clamp(popup_pos, { 0, 0 }, screen - popup->m_size); + popup->SetPosition(popup_pos); + tick->SetPosition(tick_pos.x, tick_pos.y + (target->m_size.y - tick_sz.y) * 0.5f); + popup->update(); + + popup->m_mouse_ignore = false; + popup->m_flood_events = true; + popup->m_capture_children = false; + popup->mouse_capture(); + + auto c = static_cast(target->m_children[0].get()); + panel_.m_picker->set_color(c->m_color); + panel_.m_picker->on_popup_close = [tick](Node*) { + tick->destroy(); + }; + + auto* panel = &panel_; + panel_.m_picker->on_color_change = [panel, c](Node*, glm::vec3 rgb) { + c->m_color = glm::vec4(rgb, 1.f); + if (panel->on_color_change) + panel->on_color_change(panel, rgb); + }; + } + + NodePanelQuick& panel_; + const NodePanelQuick::MiniState* restore_state_ = nullptr; +}; + +} // namespace + Node* NodePanelQuick::clone_instantiate() const { return new this_class; @@ -90,16 +278,10 @@ void NodePanelQuick::set_state(const MiniState& state, bool fire_event /*= false if (!plan) return; - for (int i = 0; i < 3; i++) - { - auto b = static_cast(m_button_brushes[i]->m_children[0].get()); - b->m_brush = state.brushes[i]; - b->draw_stroke(); - auto c = static_cast(m_button_colors[i]->m_children[0].get()); - c->m_color = state.colors[i]; - } - set_selected_color_index(state.color_index, fire_event); - set_selected_brush_index(state.brush_index, fire_event); + LegacyQuickUiServices services(*this, &state); + const auto status = pp::app::execute_quick_ui_plan(plan.value(), services); + if (!status.ok()) + LOG("Quick restore action failed: %s", status.message); } void NodePanelQuick::reset_state(bool fire_event /*= false*/) @@ -108,18 +290,10 @@ void NodePanelQuick::reset_state(bool fire_event /*= false*/) if (!plan) return; - for (int i = 0; i < 3; i++) - { - auto b = static_cast(m_button_brushes[i]->m_children[0].get()); - b->m_brush = std::make_shared(); - b->m_brush->load_tip("data/brushes/Round-Hard.png", "data/brushes/thumbs/Round-Hard.png"); - b->draw_stroke(); - } - static_cast(m_button_colors[0]->m_children[0].get())->m_color = glm::vec4(0, 0, 0, 1); - static_cast(m_button_colors[1]->m_children[0].get())->m_color = glm::vec4(.5, .5, .5, 1); - static_cast(m_button_colors[2]->m_children[0].get())->m_color = glm::vec4(1, 1, 1, 1); - set_selected_brush_index(0, fire_event); - set_selected_color_index(0, fire_event); + LegacyQuickUiServices services(*this); + const auto status = pp::app::execute_quick_ui_plan(plan.value(), services); + if (!status.ok()) + LOG("Quick reset action failed: %s", status.message); } void NodePanelQuick::init_controls() @@ -226,73 +400,10 @@ void NodePanelQuick::handle_button_brush_click(Node* button) if (!plan) return; - if (plan.value().updates_selection) - { - auto b = static_cast(button); - b->set_active(true); - m_button_brush_current->set_active(false); - m_button_brush_current = b; - m_button_brush_current_preview = static_cast(button->m_children[0].get()); - if (on_brush_change) - on_brush_change(this, m_button_brush_current_preview->m_brush); - return; - } - - if (!plan.value().opens_brush_popup) - return; - - auto popup = App::I->presets; - auto screen = root()->m_size; - glm::vec2 tick_sz = { 16, 32 }; - glm::vec2 tick_pos = button->m_pos + glm::vec2(button->m_size.x, 0); - glm::vec2 popup_pos = { tick_pos.x + tick_sz.x, tick_pos.y }; - - auto tick = root()->add_child(); - tick->SetPositioning(YGPositionTypeAbsolute); - tick->SetPosition(tick_pos.x, tick_pos.y + (button->m_size.y - tick_sz.y) * 0.5f); - tick->SetSize(tick_sz); - tick->set_image("data/ui/popup-tick.png"); - tick->m_scale = { 1, 1 }; - - float hh = popup->m_container->m_children.size() > 10 ? (screen.y - 90.f) : 400.f; - popup->SetWidth(350); - popup->SetHeight(glm::max(hh, 400.f)); - popup->SetPositioning(YGPositionTypeAbsolute); - popup->SetPosition(popup_pos); - root()->add_child(popup); - - root()->update(); - popup->tick(0); - popup->update(); - - if (tick_pos.x + popup->m_size.x > screen.x) - { - tick_pos = button->m_pos - glm::vec2(tick_sz.x, 0); - popup_pos = { tick_pos.x - popup->GetWidth(), tick_pos.y }; - tick->m_scale.x = -1.f; - } - popup_pos = glm::clamp(popup_pos, { 0, 0 }, screen - popup->m_size); - popup->SetPosition(popup_pos); - tick->SetPosition(tick_pos.x, tick_pos.y + (button->m_size.y - tick_sz.y) * 0.5f); - popup->update(); - - popup->m_mouse_ignore = false; - popup->m_flood_events = true; - popup->m_capture_children = false; - popup->mouse_capture(); - - popup->on_popup_close = [this, tick](Node*) { - tick->destroy(); - }; - - popup->on_brush_changed = [this, button](Node* target, std::shared_ptr& b) { - auto pr = static_cast(button->m_children[0].get()); - *pr->m_brush = *b; - pr->m_brush->load(); - pr->draw_stroke(); - if (on_brush_change) - on_brush_change(button, pr->m_brush); - }; + LegacyQuickUiServices services(*this); + const auto status = pp::app::execute_quick_ui_plan(plan.value(), services); + if (!status.ok()) + LOG("Quick brush action failed: %s", status.message); } void NodePanelQuick::handle_button_color_click(Node* target) @@ -307,68 +418,8 @@ void NodePanelQuick::handle_button_color_click(Node* target) if (!plan) return; - if (plan.value().updates_selection) - { - auto button = static_cast(target); - button->set_active(true); - m_button_color_current->set_active(false); - m_button_color_current = button; - m_button_color_current_inner = static_cast(m_button_color_current->m_children[0].get()); - if (on_color_change) - on_color_change(this, m_button_color_current_inner->m_color); - return; - } - - if (!plan.value().opens_color_picker) - return; - - auto popup = m_picker; - auto screen = root()->m_size; - glm::vec2 tick_sz = { 16, 32 }; - glm::vec2 tick_pos = target->m_pos + glm::vec2(target->m_size.x, 0); - glm::vec2 popup_pos = { tick_pos.x + tick_sz.x, tick_pos.y - 140.f }; - - auto tick = root()->add_child(); - tick->SetPositioning(YGPositionTypeAbsolute); - tick->SetPosition(tick_pos.x, tick_pos.y + (target->m_size.y - tick_sz.y) * 0.5f); - tick->SetSize(tick_sz); - tick->set_image("data/ui/popup-tick.png"); - tick->m_scale = { 1, 1 }; - - //float hh = popup->m_container->m_children.size() > 10 ? (screen.y / App::I->zoom - 90.f) : 400.f; - //popup->SetHeight(glm::max(hh, 400.f)); - popup->SetPositioning(YGPositionTypeAbsolute); - popup->SetPosition(popup_pos); - root()->add_child(popup); - - root()->update(); - popup->tick(0); - popup->update(); - - if (tick_pos.x + popup->m_size.x > screen.x) - { - tick_pos = target->m_pos - glm::vec2(tick_sz.x, 0); - popup_pos = { tick_pos.x - popup->GetWidth(), tick_pos.y - 140.f }; - tick->m_scale.x = -1.f; - } - popup_pos = glm::clamp(popup_pos, { 0, 0 }, screen - popup->m_size); - popup->SetPosition(popup_pos); - tick->SetPosition(tick_pos.x, tick_pos.y + (target->m_size.y - tick_sz.y) * 0.5f); - popup->update(); - - popup->m_mouse_ignore = false; - popup->m_flood_events = true; - popup->m_capture_children = false; - popup->mouse_capture(); - - auto c = static_cast(target->m_children[0].get()); - m_picker->set_color(c->m_color); - m_picker->on_popup_close = [this, tick](Node*) { - tick->destroy(); - }; - m_picker->on_color_change = [this, c](Node*, glm::vec3 rgb) { - c->m_color = glm::vec4(rgb, 1.f); - if (on_color_change) - on_color_change(this, rgb); - }; + LegacyQuickUiServices services(*this); + const auto status = pp::app::execute_quick_ui_plan(plan.value(), services); + if (!status.ok()) + LOG("Quick color action failed: %s", status.message); } diff --git a/tests/app_core/quick_ui_tests.cpp b/tests/app_core/quick_ui_tests.cpp index 720b211..b0c8b45 100644 --- a/tests/app_core/quick_ui_tests.cpp +++ b/tests/app_core/quick_ui_tests.cpp @@ -1,8 +1,57 @@ #include "app_core/quick_ui.h" #include "test_harness.h" +#include + namespace { +class FakeQuickUiServices final : public pp::app::QuickUiServices { +public: + void select_slot(pp::app::QuickUiSlotKind slot_kind, int slot_index, bool fire_event) override + { + selects += 1; + last_slot_kind = slot_kind; + last_slot_index = slot_index; + last_fire_event = fire_event; + call_order += "select;"; + } + + void open_slot_popup(pp::app::QuickUiSlotKind slot_kind, int slot_index) override + { + popups += 1; + last_slot_kind = slot_kind; + last_slot_index = slot_index; + call_order += "popup;"; + } + + void restore_state(int brush_index, int color_index, bool fire_event) override + { + restores += 1; + last_brush_index = brush_index; + last_color_index = color_index; + last_fire_event = fire_event; + call_order += "restore;"; + } + + void reset_state(bool fire_event) override + { + resets += 1; + last_fire_event = fire_event; + call_order += "reset;"; + } + + int selects = 0; + int popups = 0; + int restores = 0; + int resets = 0; + pp::app::QuickUiSlotKind last_slot_kind = pp::app::QuickUiSlotKind::brush; + int last_slot_index = -1; + int last_brush_index = -1; + int last_color_index = -1; + bool last_fire_event = false; + std::string call_order; +}; + 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); @@ -12,6 +61,7 @@ void slot_click_selects_or_opens_popup(pp::tests::Harness& harness) 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().brush_index == 2); 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); @@ -49,6 +99,8 @@ void restore_and_reset_validate_state(pp::tests::Harness& harness) PP_EXPECT(harness, restore); if (restore) { PP_EXPECT(harness, restore.value().operation == pp::app::QuickUiOperation::restore_state); + PP_EXPECT(harness, restore.value().brush_index == 2); + PP_EXPECT(harness, restore.value().color_index == 1); PP_EXPECT(harness, restore.value().slot_count == 3); PP_EXPECT(harness, restore.value().fire_event); PP_EXPECT(harness, restore.value().restores_slots); @@ -60,6 +112,8 @@ void restore_and_reset_validate_state(pp::tests::Harness& harness) PP_EXPECT(harness, reset); if (reset) { PP_EXPECT(harness, reset.value().operation == pp::app::QuickUiOperation::reset_state); + PP_EXPECT(harness, reset.value().brush_index == 0); + PP_EXPECT(harness, reset.value().color_index == 0); PP_EXPECT(harness, reset.value().resets_slots); PP_EXPECT(harness, reset.value().redraws_brush_previews); PP_EXPECT(harness, !reset.value().invokes_change_callback); @@ -71,6 +125,89 @@ void restore_and_reset_validate_state(pp::tests::Harness& harness) PP_EXPECT(harness, !pp::app::plan_quick_state_reset(0, false)); } +void executor_dispatches_selection_popup_restore_and_reset(pp::tests::Harness& harness) +{ + FakeQuickUiServices services; + + const auto select = pp::app::plan_quick_slot_click(pp::app::QuickUiSlotKind::brush, 0, 2, 3); + PP_EXPECT(harness, select); + if (select) { + PP_EXPECT(harness, pp::app::execute_quick_ui_plan(select.value(), services).ok()); + } + + const auto popup = pp::app::plan_quick_slot_click(pp::app::QuickUiSlotKind::color, 1, 1, 3); + PP_EXPECT(harness, popup); + if (popup) { + PP_EXPECT(harness, pp::app::execute_quick_ui_plan(popup.value(), services).ok()); + } + + const auto restore = pp::app::plan_quick_state_restore(2, 1, 3, true); + PP_EXPECT(harness, restore); + if (restore) { + PP_EXPECT(harness, pp::app::execute_quick_ui_plan(restore.value(), services).ok()); + } + + const auto reset = pp::app::plan_quick_state_reset(3, false); + PP_EXPECT(harness, reset); + if (reset) { + PP_EXPECT(harness, pp::app::execute_quick_ui_plan(reset.value(), services).ok()); + } + + PP_EXPECT(harness, services.selects == 1); + PP_EXPECT(harness, services.popups == 1); + PP_EXPECT(harness, services.restores == 1); + PP_EXPECT(harness, services.resets == 1); + PP_EXPECT(harness, services.last_brush_index == 2); + PP_EXPECT(harness, services.last_color_index == 1); + PP_EXPECT(harness, !services.last_fire_event); + PP_EXPECT(harness, services.call_order == "select;popup;restore;reset;"); +} + +void executor_rejects_malformed_quick_plans(pp::tests::Harness& harness) +{ + FakeQuickUiServices services; + + pp::app::QuickUiPlan select; + select.operation = pp::app::QuickUiOperation::select_slot; + select.slot_index = 0; + select.slot_count = 3; + select.updates_selection = false; + PP_EXPECT(harness, !pp::app::execute_quick_ui_plan(select, services).ok()); + + pp::app::QuickUiPlan popup; + popup.operation = pp::app::QuickUiOperation::open_slot_popup; + popup.slot_index = 0; + popup.slot_count = 3; + PP_EXPECT(harness, !pp::app::execute_quick_ui_plan(popup, services).ok()); + + pp::app::QuickUiPlan mismatched_popup; + mismatched_popup.operation = pp::app::QuickUiOperation::open_slot_popup; + mismatched_popup.slot_kind = pp::app::QuickUiSlotKind::color; + mismatched_popup.slot_index = 0; + mismatched_popup.slot_count = 3; + mismatched_popup.opens_brush_popup = true; + PP_EXPECT(harness, !pp::app::execute_quick_ui_plan(mismatched_popup, services).ok()); + + pp::app::QuickUiPlan restore; + restore.operation = pp::app::QuickUiOperation::restore_state; + restore.slot_count = 3; + restore.brush_index = 2; + restore.color_index = 3; + restore.restores_slots = true; + PP_EXPECT(harness, !pp::app::execute_quick_ui_plan(restore, services).ok()); + + pp::app::QuickUiPlan reset; + reset.operation = pp::app::QuickUiOperation::reset_state; + reset.slot_count = 0; + reset.resets_slots = true; + PP_EXPECT(harness, !pp::app::execute_quick_ui_plan(reset, services).ok()); + + PP_EXPECT(harness, services.selects == 0); + PP_EXPECT(harness, services.popups == 0); + PP_EXPECT(harness, services.restores == 0); + PP_EXPECT(harness, services.resets == 0); +} + } // namespace int main() @@ -79,5 +216,7 @@ int main() 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); + harness.run("executor dispatches selection popup restore and reset", executor_dispatches_selection_popup_restore_and_reset); + harness.run("executor rejects malformed quick plans", executor_rejects_malformed_quick_plans); return harness.finish(); } diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 5165994..7efa038 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -4742,6 +4742,8 @@ int plan_quick_operation(int argc, char** argv) << "\",\"slotKind\":\"" << quick_ui_slot_kind_name(value.slot_kind) << "\",\"slotIndex\":" << value.slot_index << ",\"previousIndex\":" << value.previous_index + << ",\"brushIndex\":" << value.brush_index + << ",\"colorIndex\":" << value.color_index << ",\"slotCount\":" << value.slot_count << ",\"fireEvent\":" << json_bool(value.fire_event) << ",\"updatesSelection\":" << json_bool(value.updates_selection)