diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 1b63bfc..ad3bba1 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -285,9 +285,13 @@ Known local toolchain state: by `DEBT-0024`. - `src/legacy_quick_ui_services.*` is the current UI-shell bridge for quick brush/color slot selection, popup routing, mini-state restore, and mini-state - reset execution. It keeps those live paths on the `pp_app_core` contracts - while legacy quick widgets, brush previews, color picker state, and preset - popup execution remain tracked by `DEBT-0025`. + reset execution. Quick size/flow slider preview cursor placement and pen/line + mode tip flags now consume the tested `pp_app_core` planner directly from + `NodePanelQuick`, and `pano_cli plan-quick-slider-preview` exposes that path + for automation. It keeps those live paths on the `pp_app_core` contracts + while legacy quick widgets, brush previews, color picker state, preset popup + execution, and direct legacy `CanvasMode*` field writes remain tracked by + `DEBT-0025`. - `pano_cli simulate-image-import` decodes an embedded tiny PNG through `pp_assets`, attaches it to `pp_document`, and is covered by `pano_cli_simulate_image_import_smoke`. diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 5dac616..05857d0 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -62,6 +62,12 @@ agent or engineer to remove them without reconstructing context from chat. that view model in the live app, and `pano_cli plan-brush-refresh` exposes the same fan-out path for automation. Retained legacy quick/stroke/color widget writes plus `Brush`/`Canvas::I` ownership remain open under DEBT-0023. +- 2026-06-05: DEBT-0025 was narrowed. Quick size/flow slider preview planning + now goes through tested `pp_app_core`, live `NodePanelQuick` slider callbacks + consume that plan for cursor placement and pen/line tip flags, and + `pano_cli plan-quick-slider-preview` exposes the path for automation. Legacy + quick widgets, brush previews, popup state, and direct `CanvasMode*` field + writes remain open under DEBT-0025. - 2026-06-04: DEBT-0036 was narrowed again. Canvas stroke commit, thumbnail, and object-draw history paths now query saved blend state through tested `pp_renderer_gl` capability-state dispatch; CanvasLayer equirect @@ -100,7 +106,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0022 | Open | Modernization | Animation panel frame command planning, panel action planning, panel view-model projection, timeline scrub planning, panel-control/timeline execution dispatch, selected-frame click dispatch, playback tick stepping, and play-mode toggles now consume pure `pp_app_core` through `NodePanelAnimation`, `NodeAnimationTimeline`, `pano_cli plan-animation-operation`, `pano_cli plan-animation-panel-action`, `pano_cli plan-animation-panel-view`, `pano_cli plan-animation-timeline-scrub`, and `DocumentAnimationServices`; live execution is centralized in `src/legacy_document_animation_services.*`, but that bridge still mutates or reads legacy `Canvas`/`Layer` frame state, canvas mode, animation-panel timeline/playback fields, and uses a temporary `NodePanelAnimation` friend adapter | 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 select --frame-count 3 --selected-frame 1 --layer-index 2 --layer-id 42`; `pano_cli plan-animation-operation --kind playback --total-duration 5 --current-frame 4 --offset 1`; `pano_cli plan-animation-operation --kind toggle-playback --playing`; `pano_cli plan-animation-panel-action --action next --total-duration 5 --current-frame 4`; `pano_cli plan-animation-panel-view --layer-count 2 --frame-count 3 --total-duration 6 --current-layer 1 --current-frame 4`; `pano_cli plan-animation-timeline-scrub --total-duration 5 --cursor-x 174.99`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline/playback execution is owned by injected document/app timeline services with no legacy `Canvas`/`Layer`/canvas-mode adapter and UI nodes acting only as adapters or removed entirely | | DEBT-0023 | Open | Modernization | Brush/color/preset/stroke-settings UI planning, texture-list add/remove/reorder planning, brush preset-list add/select/move/remove/clear planning, stroke-panel slider/toggle/blend/reset planning, stroke-panel view projection, app-level brush refresh view projection, and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, `App::brush_update()`, `NodePanelBrush`, `NodePanelBrushPreset`, `NodePanelStroke`, `NodePanelStroke::update_controls()`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, `pano_cli plan-brush-refresh`, `pano_cli plan-brush-texture-list`, `pano_cli plan-brush-preset-list`, `pano_cli plan-brush-stroke-control`, `pano_cli plan-brush-stroke-panel-view`, `BrushUiServices`, `BrushTextureListServices`, `BrushPresetListServices`, and `BrushStrokeControlServices`, and live execution is centralized in `src/legacy_brush_ui_services.*` or narrow legacy service bridges where possible, but preset-list execution still mutates legacy `NodePanelBrushPreset` child nodes directly while the bridge and panel adapter still mutate/read legacy `Brush`/`Canvas::I`, load/save legacy brush texture images, apply retained legacy quick/stroke/color widget writes, own brush thumbnail paths and popup behavior, and use temporary `NodePanelBrush`/`NodePanelBrushPreset` friend adapters to reach private list state | Preserve existing brush UI behavior while brush commands and view projection move toward a brush/app/asset command boundary and asset-managed texture/preset 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`; `pano_cli plan-brush-refresh --floating-picker --tip-flow 0.8 --tip-size 48 --r 0.2 --g 0.3 --b 0.4 --a 1`; `pano_cli plan-brush-texture-list --kind add --dir brushes --data-path data --source C:/Temp/soft.png`; `pano_cli plan-brush-preset-list --kind remove --item-count 1 --current-index 0`; `pano_cli plan-brush-stroke-control --kind float --setting tip-size --value 42.5`; `pano_cli plan-brush-stroke-control --kind blend --setting pattern --blend-mode 3`; `pano_cli plan-brush-stroke-panel-view --tip-size 64 --jitter-scatter 0.4 --dual-disabled --tip-blend-mode 2 --pattern-blend-mode 5`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset/stroke-settings, texture-list, preset-list, stroke-control execution, stroke-panel projection, and brush refresh projection are owned by injected brush/app/asset/UI services with no legacy brush/canvas adapter, direct `NodePanelBrushPreset` child mutation, brush thumbnail/popup ownership, legacy quick/stroke/color widget writes, or brush-panel friend access | | DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning and execution dispatch now consume pure `pp_app_core` through `NodePanelGrid`, `pano_cli plan-grid-operation`, and the `GridUiServices` boundary; live execution is centralized in `src/legacy_grid_ui_services.*`, and retained CPU lightmap row dispatch now uses shared `parallel_for` instead of platform-specific Win32/Apple worker APIs, but the bridge still performs legacy image loading, OpenGL texture updates, nanort lightmap baking/progress, and `Canvas::draw_objects` commit execution | 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 and execution dispatch now consume pure `pp_app_core` through `NodePanelQuick`, `pano_cli plan-quick-operation`, and the `QuickUiServices` boundary; live execution is centralized in `src/legacy_quick_ui_services.*`, but the bridge 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-0025 | Open | Modernization | Quick brush/color slot, mini-state, and size/flow slider preview planning now consume pure `pp_app_core` through `NodePanelQuick`, `pano_cli plan-quick-operation`, `pano_cli plan-quick-slider-preview`, and the `QuickUiServices` boundary; live slot/popup/restore/reset execution is centralized in `src/legacy_quick_ui_services.*`, and live slider callbacks now consume `pp_app_core` preview cursor/tip planning directly, but the bridge and panel adapter still mutate legacy quick UI widgets, `Brush` previews, color picker popup state, preset popup state, and direct legacy `CanvasModePen`/`CanvasModeLine` fields | Preserve quick-panel behavior while quick brush/color and slider preview 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`; `pano_cli plan-quick-slider-preview --slider-x 10 --slider-y 20 --slider-height 40 --zoom 2 --pen-mode --no-line-mode`; `ctest --preset desktop-fast --build-config Debug` | Quick-panel selection, popup, restore, reset, brush preview, color execution, and slider preview mode updates are owned by injected app/brush/UI/canvas services with no legacy quick-panel adapter, popup adapter, direct brush-preview mutation, or direct `CanvasMode*` field writes | | DEBT-0026 | Open | Modernization | Toolbar history command planning and canvas hotkey history dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, `pano_cli plan-history-operation`, and the `HistoryUiServices` boundary, and both live callers share `src/legacy_history_services.*` for saturated legacy history metrics and execution, but the shared live bridge 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, active-state planning/execution dispatch, and canvas keyboard/touch command planning now consume pure `pp_app_core` through `App::init_toolbar_draw`, `App::update`, `NodeCanvas`, `pano_cli plan-canvas-tool`, `pano_cli plan-canvas-tool-state`, `pano_cli plan-canvas-hotkey`, `CanvasToolServices`, and `CanvasHotkeyServices`, live toolbar/input/hotkey execution is centralized in `src/legacy_canvas_tool_services.*`, and canvas mode tip visibility plus pressure remapping now route through `PlatformServices`, but the bridge still mutates or reads legacy `Canvas` mode state, pen picking state, touch-lock state, transform copy/cut action objects, `ActionManager`, legacy save UI, legacy stroke size controls, and cursor/UI singletons | Preserve current toolbar, stylus eraser, keyboard, and touch command behavior while canvas input/tools move toward an app/document command boundary | `pp_app_core_canvas_tool_ui_tests`; `pp_app_core_canvas_hotkey_tests`; `pp_platform_api_tests`; `pano_cli plan-canvas-tool --kind copy`; `pano_cli plan-canvas-tool-state --mode draw --picking --touch-lock`; `pano_cli plan-canvas-hotkey --event key-up --key z --ctrl --undo-count 2`; `pano_cli plan-canvas-hotkey --event key-up --key s --ctrl --shift`; `ctest --preset desktop-fast --build-config Debug` | Canvas tool selection, toolbar state refresh, picking, touch lock, stylus eraser/key mode switching, hotkey/touch command dispatch, save hotkeys, history hotkeys, brush-size hotkeys, and transform action execution are owned by injected app/document/canvas services with no legacy toolbar/canvas adapter | | DEBT-0028 | Open | Modernization | Canvas clear command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, Layer menu clear, `pano_cli plan-canvas-clear`, and the `DocumentCanvasClearServices` boundary, and toolbar/Layer-menu clear share `src/legacy_document_canvas_services.*`, but the shared live bridge 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`; `pano_cli plan-layer-menu --command clear --current-index 1 --current-name Paint`; `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 1698de6..8ed15ad 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -630,6 +630,10 @@ slot selection versus popup opening, plus quick mini-state restore/reset validation used by the live quick panel. Quick-panel execution now dispatches through `QuickUiServices` in `src/legacy_quick_ui_services.*` before the legacy `Brush`, color picker, stroke preview, and preset popup adapter continues. +`pano_cli plan-quick-slider-preview` exposes app-core planning for quick +size/flow slider preview cursor placement, RTL offset handling, and pen/line +mode tip flags; live `NodePanelQuick` slider callbacks now consume that plan +before the retained `CanvasModePen`/`CanvasModeLine` and brush-preview updates. `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, diff --git a/src/app_core/quick_ui.h b/src/app_core/quick_ui.h index 0e7ee71..4cebc6d 100644 --- a/src/app_core/quick_ui.h +++ b/src/app_core/quick_ui.h @@ -2,6 +2,8 @@ #include "foundation/result.h" +#include + namespace pp::app { enum class QuickUiSlotKind { @@ -35,6 +37,27 @@ struct QuickUiPlan { bool mutates_quick_state = false; }; +struct QuickSliderPreviewInput { + bool ui_rtl = false; + float slider_x = 0.0F; + float slider_y = 0.0F; + float slider_height = 0.0F; + float zoom = 1.0F; + bool has_pen_mode = false; + bool has_line_mode = false; +}; + +struct QuickSliderPreviewPlan { + float cursor_x = 0.0F; + float cursor_y = 0.0F; + bool updates_pen_mode = false; + bool updates_line_mode = false; + bool draws_tip = false; + bool disables_pen_outline = false; + bool redraws_brush_preview = false; + bool invokes_change_callback = false; +}; + class QuickUiServices { public: virtual ~QuickUiServices() = default; @@ -45,6 +68,15 @@ public: virtual void reset_state(bool fire_event) = 0; }; +[[nodiscard]] inline pp::foundation::Status validate_quick_slider_float(float value) noexcept +{ + if (!std::isfinite(value)) { + return pp::foundation::Status::invalid_argument("quick slider preview value must be finite"); + } + + return pp::foundation::Status::success(); +} + [[nodiscard]] inline pp::foundation::Status validate_quick_slot_count(int slot_count) noexcept { if (slot_count <= 0) { @@ -68,6 +100,47 @@ public: return pp::foundation::Status::success(); } +[[nodiscard]] inline pp::foundation::Result plan_quick_slider_preview( + const QuickSliderPreviewInput& input) +{ + const auto x_status = validate_quick_slider_float(input.slider_x); + if (!x_status.ok()) { + return pp::foundation::Result::failure(x_status); + } + const auto y_status = validate_quick_slider_float(input.slider_y); + if (!y_status.ok()) { + return pp::foundation::Result::failure(y_status); + } + const auto height_status = validate_quick_slider_float(input.slider_height); + if (!height_status.ok()) { + return pp::foundation::Result::failure(height_status); + } + if (input.slider_height < 0.0F) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("quick slider preview height must not be negative")); + } + const auto zoom_status = validate_quick_slider_float(input.zoom); + if (!zoom_status.ok()) { + return pp::foundation::Result::failure(zoom_status); + } + if (input.zoom <= 0.0F) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("quick slider preview zoom must be positive")); + } + + const float offset = input.ui_rtl ? -100.0F : 100.0F; + QuickSliderPreviewPlan plan; + plan.cursor_x = (input.slider_x + offset) * input.zoom; + plan.cursor_y = (input.slider_y + input.slider_height * 0.5F) * input.zoom; + plan.updates_pen_mode = input.has_pen_mode; + plan.updates_line_mode = input.has_line_mode; + plan.draws_tip = input.has_pen_mode || input.has_line_mode; + plan.disables_pen_outline = input.has_pen_mode; + plan.redraws_brush_preview = true; + plan.invokes_change_callback = true; + return pp::foundation::Result::success(plan); +} + [[nodiscard]] inline pp::foundation::Result plan_quick_slot_click( QuickUiSlotKind slot_kind, int current_index, diff --git a/src/node_panel_quick.cpp b/src/node_panel_quick.cpp index 6081dc8..b52d4d2 100644 --- a/src/node_panel_quick.cpp +++ b/src/node_panel_quick.cpp @@ -121,39 +121,13 @@ void NodePanelQuick::init_controls() m_slider_size = find("quick-size"); m_slider_size->on_value_changed = [this](Node* target, float value) { - float off = App::I->ui_rtl ? -100.f : 100.f; - auto newpos = (m_slider_flow->m_pos + glm::vec2(off, m_slider_flow->m_size.y / 2.f)) * App::I->zoom; - if (auto m = dynamic_cast(Canvas::I->modes[(int)Canvas::I->m_current_mode][0])) - { - m->m_cur_pos = newpos; - m->m_draw_tip = true; - m->m_draw_outline = false; - } - if (auto m = dynamic_cast(Canvas::I->modes[(int)Canvas::I->m_current_mode][0])) - { - m->m_cur_pos = newpos; - m->m_draw_tip = true; - } - m_button_brush_current_preview->draw_stroke(); + update_slider_preview(); if (on_size_change) on_size_change(target, value); }; m_slider_flow = find("quick-flow"); m_slider_flow->on_value_changed = [this](Node* target, float value) { - float off = App::I->ui_rtl ? -100.f : 100.f; - auto newpos = (m_slider_flow->m_pos + glm::vec2(off, m_slider_flow->m_size.y / 2.f)) * App::I->zoom; - if (auto m = dynamic_cast(Canvas::I->modes[(int)Canvas::I->m_current_mode][0])) - { - m->m_cur_pos = newpos; - m->m_draw_tip = true; - m->m_draw_outline = false; - } - if (auto m = dynamic_cast(Canvas::I->modes[(int)Canvas::I->m_current_mode][0])) - { - m->m_cur_pos = newpos; - m->m_draw_tip = true; - } - m_button_brush_current_preview->draw_stroke(); + update_slider_preview(); if (on_flow_change) on_flow_change(target, value); }; @@ -199,6 +173,45 @@ NodeButtonCustom* NodePanelQuick::init_button_brush(const std::string& name, boo return button; } +void NodePanelQuick::update_slider_preview() +{ + if (!App::I || !Canvas::I || !Canvas::I->m_mode || Canvas::I->m_mode->empty() || !m_slider_flow || !m_button_brush_current_preview) + return; + + auto* mode = (*Canvas::I->m_mode)[0]; + auto* pen_mode = dynamic_cast(mode); + auto* line_mode = dynamic_cast(mode); + + const auto plan = pp::app::plan_quick_slider_preview(pp::app::QuickSliderPreviewInput { + .ui_rtl = App::I->ui_rtl, + .slider_x = m_slider_flow->m_pos.x, + .slider_y = m_slider_flow->m_pos.y, + .slider_height = m_slider_flow->m_size.y, + .zoom = App::I->zoom, + .has_pen_mode = pen_mode != nullptr, + .has_line_mode = line_mode != nullptr, + }); + if (!plan) { + LOG("Quick slider preview failed: %s", plan.status().message); + return; + } + + const glm::vec2 cursor(plan.value().cursor_x, plan.value().cursor_y); + if (plan.value().updates_pen_mode && pen_mode) + { + pen_mode->m_cur_pos = cursor; + pen_mode->m_draw_tip = plan.value().draws_tip; + pen_mode->m_draw_outline = !plan.value().disables_pen_outline; + } + if (plan.value().updates_line_mode && line_mode) + { + line_mode->m_cur_pos = cursor; + line_mode->m_draw_tip = plan.value().draws_tip; + } + if (plan.value().redraws_brush_preview) + m_button_brush_current_preview->draw_stroke(); +} + void NodePanelQuick::handle_button_brush_click(Node* button) { const auto clicked = std::find(m_button_brushes.begin(), m_button_brushes.end(), button); diff --git a/src/node_panel_quick.h b/src/node_panel_quick.h index c63521e..9b8f49d 100644 --- a/src/node_panel_quick.h +++ b/src/node_panel_quick.h @@ -51,6 +51,7 @@ public: private: void init_controls(); NodeButtonCustom* init_button_brush(const std::string& name, bool szp, bool flp); + void update_slider_preview(); void handle_button_brush_click(Node* target); void handle_button_color_click(Node* target); }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1a66fb5..799a7eb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1748,6 +1748,30 @@ if(TARGET pano_cli) LABELS "app;ui;paint;integration;desktop-fast;fuzz" WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_quick_slider_preview_pen_smoke + COMMAND pano_cli plan-quick-slider-preview --slider-x 10 --slider-y 20 --slider-height 40 --zoom 2 --pen-mode --no-line-mode) + set_tests_properties(pano_cli_plan_quick_slider_preview_pen_smoke PROPERTIES + LABELS "app;ui;paint;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-quick-slider-preview\".*\"cursorX\":220.*\"cursorY\":80.*\"updatesPenMode\":true.*\"updatesLineMode\":false.*\"drawsTip\":true.*\"disablesPenOutline\":true.*\"redrawsBrushPreview\":true") + + add_test(NAME pano_cli_plan_quick_slider_preview_rtl_line_smoke + COMMAND pano_cli plan-quick-slider-preview --rtl --slider-x 50 --slider-y 10 --slider-height 20 --zoom 1.5 --no-pen-mode --line-mode) + set_tests_properties(pano_cli_plan_quick_slider_preview_rtl_line_smoke PROPERTIES + LABELS "app;ui;paint;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-quick-slider-preview\".*\"rtl\":true.*\"cursorX\":-75.*\"cursorY\":30.*\"updatesPenMode\":false.*\"updatesLineMode\":true.*\"drawsTip\":true.*\"disablesPenOutline\":false") + + add_test(NAME pano_cli_plan_quick_slider_preview_rejects_bad_float + COMMAND pano_cli plan-quick-slider-preview --bad-float) + set_tests_properties(pano_cli_plan_quick_slider_preview_rejects_bad_float PROPERTIES + LABELS "app;ui;paint;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + + add_test(NAME pano_cli_plan_quick_slider_preview_rejects_zero_zoom + COMMAND pano_cli plan-quick-slider-preview --zoom 0) + set_tests_properties(pano_cli_plan_quick_slider_preview_rejects_zero_zoom 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 index b0c8b45..366ab94 100644 --- a/tests/app_core/quick_ui_tests.cpp +++ b/tests/app_core/quick_ui_tests.cpp @@ -1,6 +1,7 @@ #include "app_core/quick_ui.h" #include "test_harness.h" +#include #include namespace { @@ -125,6 +126,66 @@ void restore_and_reset_validate_state(pp::tests::Harness& harness) PP_EXPECT(harness, !pp::app::plan_quick_state_reset(0, false)); } +void slider_preview_projects_cursor_and_mode_flags(pp::tests::Harness& harness) +{ + const auto ltr = pp::app::plan_quick_slider_preview(pp::app::QuickSliderPreviewInput { + .ui_rtl = false, + .slider_x = 10.0F, + .slider_y = 20.0F, + .slider_height = 40.0F, + .zoom = 2.0F, + .has_pen_mode = true, + .has_line_mode = false, + }); + PP_EXPECT(harness, ltr); + if (ltr) { + PP_EXPECT(harness, ltr.value().cursor_x == 220.0F); + PP_EXPECT(harness, ltr.value().cursor_y == 80.0F); + PP_EXPECT(harness, ltr.value().updates_pen_mode); + PP_EXPECT(harness, !ltr.value().updates_line_mode); + PP_EXPECT(harness, ltr.value().draws_tip); + PP_EXPECT(harness, ltr.value().disables_pen_outline); + PP_EXPECT(harness, ltr.value().redraws_brush_preview); + PP_EXPECT(harness, ltr.value().invokes_change_callback); + } + + const auto rtl_line = pp::app::plan_quick_slider_preview(pp::app::QuickSliderPreviewInput { + .ui_rtl = true, + .slider_x = 50.0F, + .slider_y = 10.0F, + .slider_height = 20.0F, + .zoom = 1.5F, + .has_pen_mode = false, + .has_line_mode = true, + }); + PP_EXPECT(harness, rtl_line); + if (rtl_line) { + PP_EXPECT(harness, rtl_line.value().cursor_x == -75.0F); + PP_EXPECT(harness, rtl_line.value().cursor_y == 30.0F); + PP_EXPECT(harness, !rtl_line.value().updates_pen_mode); + PP_EXPECT(harness, rtl_line.value().updates_line_mode); + PP_EXPECT(harness, rtl_line.value().draws_tip); + PP_EXPECT(harness, !rtl_line.value().disables_pen_outline); + } +} + +void slider_preview_rejects_invalid_geometry(pp::tests::Harness& harness) +{ + PP_EXPECT(harness, !pp::app::plan_quick_slider_preview(pp::app::QuickSliderPreviewInput { + .slider_x = std::nanf(""), + .slider_height = 1.0F, + .zoom = 1.0F, + })); + PP_EXPECT(harness, !pp::app::plan_quick_slider_preview(pp::app::QuickSliderPreviewInput { + .slider_height = -1.0F, + .zoom = 1.0F, + })); + PP_EXPECT(harness, !pp::app::plan_quick_slider_preview(pp::app::QuickSliderPreviewInput { + .slider_height = 1.0F, + .zoom = 0.0F, + })); +} + void executor_dispatches_selection_popup_restore_and_reset(pp::tests::Harness& harness) { FakeQuickUiServices services; @@ -216,6 +277,8 @@ 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("slider preview projects cursor and mode flags", slider_preview_projects_cursor_and_mode_flags); + harness.run("slider preview rejects invalid geometry", slider_preview_rejects_invalid_geometry); 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 5c53ba3..e2659a0 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -525,6 +525,17 @@ struct PlanQuickOperationArgs { bool fire_event = false; }; +struct PlanQuickSliderPreviewArgs { + bool rtl = false; + float slider_x = 10.0F; + float slider_y = 20.0F; + float slider_height = 40.0F; + float zoom = 2.0F; + bool pen_mode = true; + bool line_mode = false; + bool bad_float = false; +}; + struct PlanToolsMenuArgs { std::string command = "shortcuts"; bool sonarpen_available = false; @@ -1996,6 +2007,7 @@ void print_help() << " plan-history-operation --kind undo|redo|clear [--undo-count N] [--redo-count N] [--memory-bytes N]\n" << " plan-main-toolbar --command open|save|undo|redo|clear-history|clear-canvas|message-box|settings [--undo-count N] [--redo-count N] [--memory-bytes N] [--no-canvas]\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-quick-slider-preview [--rtl] [--slider-x N] [--slider-y N] [--slider-height N] [--zoom N] [--pen-mode|--no-pen-mode] [--line-mode|--no-line-mode] [--bad-float]\n" << " plan-share-file [--path FILE]\n" << " plan-picked-path [--path FILE]\n" << " plan-display-file [--path FILE]\n" @@ -7129,6 +7141,94 @@ int plan_quick_operation(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_quick_slider_preview_args( + int argc, + char** argv, + PlanQuickSliderPreviewArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--slider-x" || key == "--slider-y" || key == "--slider-height" || key == "--zoom") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = parse_float_arg(argv[++i]); + if (!value) { + return value.status(); + } + if (key == "--slider-x") { + args.slider_x = value.value(); + } else if (key == "--slider-y") { + args.slider_y = value.value(); + } else if (key == "--slider-height") { + args.slider_height = value.value(); + } else { + args.zoom = value.value(); + } + } else if (key == "--rtl") { + args.rtl = true; + } else if (key == "--pen-mode") { + args.pen_mode = true; + } else if (key == "--no-pen-mode") { + args.pen_mode = false; + } else if (key == "--line-mode") { + args.line_mode = true; + } else if (key == "--no-line-mode") { + args.line_mode = false; + } else if (key == "--bad-float") { + args.bad_float = true; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_quick_slider_preview(int argc, char** argv) +{ + PlanQuickSliderPreviewArgs args; + const auto status = parse_plan_quick_slider_preview_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-quick-slider-preview", status.message); + return 2; + } + + const auto plan = pp::app::plan_quick_slider_preview(pp::app::QuickSliderPreviewInput { + .ui_rtl = args.rtl, + .slider_x = args.bad_float ? std::nanf("") : args.slider_x, + .slider_y = args.slider_y, + .slider_height = args.slider_height, + .zoom = args.zoom, + .has_pen_mode = args.pen_mode, + .has_line_mode = args.line_mode, + }); + if (!plan) { + print_error("plan-quick-slider-preview", plan.status().message); + return 2; + } + + const auto& value = plan.value(); + std::cout << "{\"ok\":true,\"command\":\"plan-quick-slider-preview\"" + << ",\"state\":{\"rtl\":" << json_bool(args.rtl) + << ",\"sliderX\":" << args.slider_x + << ",\"sliderY\":" << args.slider_y + << ",\"sliderHeight\":" << args.slider_height + << ",\"zoom\":" << args.zoom + << ",\"penMode\":" << json_bool(args.pen_mode) + << ",\"lineMode\":" << json_bool(args.line_mode) + << "},\"plan\":{\"cursorX\":" << value.cursor_x + << ",\"cursorY\":" << value.cursor_y + << ",\"updatesPenMode\":" << json_bool(value.updates_pen_mode) + << ",\"updatesLineMode\":" << json_bool(value.updates_line_mode) + << ",\"drawsTip\":" << json_bool(value.draws_tip) + << ",\"disablesPenOutline\":" << json_bool(value.disables_pen_outline) + << ",\"redrawsBrushPreview\":" << json_bool(value.redraws_brush_preview) + << ",\"invokesChangeCallback\":" << json_bool(value.invokes_change_callback) + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_share_file_args( int argc, char** argv, @@ -9669,6 +9769,10 @@ int main(int argc, char** argv) return plan_quick_operation(argc, argv); } + if (command == "plan-quick-slider-preview") { + return plan_quick_slider_preview(argc, argv); + } + if (command == "plan-share-file") { return plan_share_file(argc, argv); }