Route canvas cursor visibility through app core

This commit is contained in:
2026-06-05 01:39:36 +02:00
parent e95861e9b7
commit f42a6540be
8 changed files with 447 additions and 23 deletions

View File

@@ -1,7 +1,7 @@
# Build And Platform Inventory # Build And Platform Inventory
Status: live Status: live
Last updated: 2026-06-04 Last updated: 2026-06-05
This inventory records the known build surfaces during the CMake migration. This inventory records the known build surfaces during the CMake migration.
Keep it updated as platform paths move to shared CMake targets. Keep it updated as platform paths move to shared CMake targets.
@@ -245,9 +245,12 @@ Known local toolchain state:
canvas toolbar tool selection, NodeCanvas stylus/input mode switching, and canvas toolbar tool selection, NodeCanvas stylus/input mode switching, and
canvas hotkey/touch execution. It keeps those live paths on the `pp_app_core` canvas hotkey/touch execution. It keeps those live paths on the `pp_app_core`
contracts while canvas mode tip visibility and pressure remapping now ask contracts while canvas mode tip visibility and pressure remapping now ask
`PlatformServices`; legacy `Canvas` mode state, transform actions, picking, `PlatformServices`; `NodeCanvas::update_cursor()` now consumes
touch-lock, save/UI/cursor calls, brush-size controls, and history execution `pp_app_core` canvas cursor visibility planning exposed through
remain tracked by `DEBT-0027`. `pano_cli plan-canvas-cursor` before retained platform cursor dispatch.
Legacy `Canvas` mode state, transform actions, picking, touch-lock,
save/UI/cursor calls, brush-size controls, and history execution remain
tracked by `DEBT-0027`.
- `src/legacy_document_animation_services.*` is the current UI-shell bridge for - `src/legacy_document_animation_services.*` is the current UI-shell bridge for
animation frame commands, timeline/selected-frame execution, playback ticks, animation frame commands, timeline/selected-frame execution, playback ticks,
onion-size updates, and play-mode toggles. It keeps those live paths on the onion-size updates, and play-mode toggles. It keeps those live paths on the

View File

@@ -68,6 +68,13 @@ agent or engineer to remove them without reconstructing context from chat.
`pano_cli plan-quick-slider-preview` exposes the path for automation. Legacy `pano_cli plan-quick-slider-preview` exposes the path for automation. Legacy
quick widgets, brush previews, popup state, and direct `CanvasMode*` field quick widgets, brush previews, popup state, and direct `CanvasMode*` field
writes remain open under DEBT-0025. writes remain open under DEBT-0025.
- 2026-06-05: DEBT-0027 was narrowed. Canvas cursor visibility policy now goes
through tested `pp_app_core`, live `NodeCanvas::update_cursor()` consumes the
planner before retained platform cursor dispatch, and
`pano_cli plan-canvas-cursor` exposes draw/erase versus non-paint mode,
small-brush, not-painting, modifier, and malformed-brush states for
automation. Legacy `Canvas`/`CanvasModePen` state reads and app cursor
execution remain open under DEBT-0027.
- 2026-06-04: DEBT-0036 was narrowed again. Canvas stroke commit, - 2026-06-04: DEBT-0036 was narrowed again. Canvas stroke commit,
thumbnail, and object-draw history paths now query saved blend state through thumbnail, and object-draw history paths now query saved blend state through
tested `pp_renderer_gl` capability-state dispatch; CanvasLayer equirect tested `pp_renderer_gl` capability-state dispatch; CanvasLayer equirect
@@ -108,7 +115,7 @@ agent or engineer to remove them without reconstructing context from chat.
| 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-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, 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-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-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-0027 | Open | Modernization | Canvas draw-tool toolbar command, canvas input mode switching, active-state planning/execution dispatch, canvas cursor visibility planning, 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-cursor`, `pano_cli plan-canvas-hotkey`, `CanvasToolServices`, and `CanvasHotkeyServices`, live toolbar/input/hotkey execution is centralized in `src/legacy_canvas_tool_services.*`, `NodeCanvas::update_cursor()` consumes the cursor planner before retained platform cursor dispatch, and canvas mode tip visibility plus pressure remapping now route through `PlatformServices`, but the bridge still mutates or reads legacy `Canvas` mode state, `CanvasModePen` drawing/resizing/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, cursor visibility, 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-cursor --mode draw --visibility small-brush --brush-size 9.5`; `pano_cli plan-canvas-cursor --visibility small-brush --bad-size`; `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, cursor visibility planning/execution, 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 | | 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 |
| DEBT-0029 | Open | Modernization | Image import route planning and execution dispatch now consume pure `pp_app_core` through the File menu, `pano_cli plan-image-import`, and the `DocumentImageImportServices` boundary, and live File-menu import execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still loads images with legacy `Image`, calls legacy `Canvas::import_equirectangular`, or configures legacy import transform mode directly | Preserve current File > Import behavior while image import moves toward document/app/asset command services | `pp_app_core_document_import_tests`; `pano_cli plan-image-import --width 4096 --height 2048`; `pano_cli plan-image-import --width 1024 --height 1024`; `ctest --preset desktop-fast --build-config Debug` | Image loading, equirectangular import, transform-placement import, and failure reporting are owned by injected document/app/asset services with File-menu callbacks acting only as adapters and no legacy image-import adapter | | DEBT-0029 | Open | Modernization | Image import route planning and execution dispatch now consume pure `pp_app_core` through the File menu, `pano_cli plan-image-import`, and the `DocumentImageImportServices` boundary, and live File-menu import execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still loads images with legacy `Image`, calls legacy `Canvas::import_equirectangular`, or configures legacy import transform mode directly | Preserve current File > Import behavior while image import moves toward document/app/asset command services | `pp_app_core_document_import_tests`; `pano_cli plan-image-import --width 4096 --height 2048`; `pano_cli plan-image-import --width 1024 --height 1024`; `ctest --preset desktop-fast --build-config Debug` | Image loading, equirectangular import, transform-placement import, and failure reporting are owned by injected document/app/asset services with File-menu callbacks acting only as adapters and no legacy image-import adapter |
| DEBT-0030 | Open | Modernization | File export menu action planning and execution dispatch now consume pure `pp_app_core` through the File menu, `pano_cli plan-export-menu`, and the `DocumentExportMenuServices` boundary, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still opens legacy export dialogs and then reaches legacy canvas/render/video export code | Preserve current export menu behavior while export command execution moves toward document/app/renderer/video services | `pp_app_core_document_export_tests`; `pano_cli plan-export-menu --kind png`; `pano_cli plan-export-menu --kind animation-mp4 --demo`; `pano_cli plan-export-menu --kind layers --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Export menu routing, license gating, target creation, image/layer/cube/depth/animation/timelapse execution, and error reporting are owned by injected document/app/renderer/video services with File-menu callbacks acting only as UI adapters and no legacy export adapter | | DEBT-0030 | Open | Modernization | File export menu action planning and execution dispatch now consume pure `pp_app_core` through the File menu, `pano_cli plan-export-menu`, and the `DocumentExportMenuServices` boundary, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still opens legacy export dialogs and then reaches legacy canvas/render/video export code | Preserve current export menu behavior while export command execution moves toward document/app/renderer/video services | `pp_app_core_document_export_tests`; `pano_cli plan-export-menu --kind png`; `pano_cli plan-export-menu --kind animation-mp4 --demo`; `pano_cli plan-export-menu --kind layers --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Export menu routing, license gating, target creation, image/layer/cube/depth/animation/timelapse execution, and error reporting are owned by injected document/app/renderer/video services with File-menu callbacks acting only as UI adapters and no legacy export adapter |

View File

@@ -579,7 +579,11 @@ touch command handling now consumes
`pp_app_core` canvas-hotkey planning for E draw/erase, Ctrl+Z, Ctrl+Shift+Z, `pp_app_core` canvas-hotkey planning for E draw/erase, Ctrl+Z, Ctrl+Shift+Z,
Ctrl+S, Ctrl+Shift+S, Tab UI toggle, brush-size brackets, Android back, Alt Ctrl+S, Ctrl+Shift+S, Tab UI toggle, brush-size brackets, Android back, Alt
cursor reveal, and two-finger undo before the shared bridge delegates to legacy cursor reveal, and two-finger undo before the shared bridge delegates to legacy
UI/canvas/history adapters. UI/canvas/history adapters. `pano_cli plan-canvas-cursor` exposes the
canvas-specific cursor visibility policy for draw/erase versus non-paint
modes, small-brush thresholds, active-stroke hiding, and modifier/tool forced
visibility; live `NodeCanvas::update_cursor()` consumes that planner before
retained `App::show_cursor`/`App::hide_cursor` platform dispatch.
`pano_cli plan-canvas-clear` exposes app-core planning for the main toolbar `pano_cli plan-canvas-clear` exposes app-core planning for the main toolbar
clear-current-layer command, including clear color validation, no-canvas clear-current-layer command, including clear color validation, no-canvas
handling, undo recording intent, and dirty-state intent; live toolbar execution handling, undo recording intent, and dirty-state intent; live toolbar execution

View File

@@ -2,6 +2,8 @@
#include "foundation/result.h" #include "foundation/result.h"
#include <cmath>
namespace pp::app { namespace pp::app {
enum class CanvasToolOperation { enum class CanvasToolOperation {
@@ -30,6 +32,13 @@ enum class CanvasToolTransformAction {
cut, cut,
}; };
enum class CanvasCursorVisibilityMode {
never,
small_brush,
not_painting,
always,
};
struct CanvasToolPlan { struct CanvasToolPlan {
CanvasToolOperation operation = CanvasToolOperation::select_mode; CanvasToolOperation operation = CanvasToolOperation::select_mode;
CanvasToolMode mode = CanvasToolMode::draw; CanvasToolMode mode = CanvasToolMode::draw;
@@ -59,6 +68,25 @@ struct CanvasToolButtonState {
bool flood_fill_active = false; bool flood_fill_active = false;
}; };
struct CanvasCursorVisibilityInput {
CanvasToolMode mode = CanvasToolMode::draw;
CanvasCursorVisibilityMode visibility_mode = CanvasCursorVisibilityMode::never;
bool has_current_brush = true;
float brush_tip_size = 0.0F;
bool pen_is_drawing = false;
bool alt_down = false;
bool pen_is_resizing = false;
bool pen_is_picking = false;
};
struct CanvasCursorVisibilityPlan {
bool visible = true;
bool paint_mode = false;
bool uses_brush_size = false;
bool uses_pen_state = false;
bool forced_visible_by_modifier_or_tool = false;
};
class CanvasToolServices { class CanvasToolServices {
public: public:
virtual ~CanvasToolServices() = default; virtual ~CanvasToolServices() = default;
@@ -134,6 +162,54 @@ public:
return state; return state;
} }
[[nodiscard]] inline constexpr bool canvas_tool_mode_is_paint(CanvasToolMode mode) noexcept
{
return mode == CanvasToolMode::draw || mode == CanvasToolMode::erase;
}
[[nodiscard]] inline pp::foundation::Result<CanvasCursorVisibilityPlan> plan_canvas_cursor_visibility(
const CanvasCursorVisibilityInput& input)
{
CanvasCursorVisibilityPlan plan;
plan.paint_mode = canvas_tool_mode_is_paint(input.mode);
if (!plan.paint_mode) {
plan.visible = true;
return pp::foundation::Result<CanvasCursorVisibilityPlan>::success(plan);
}
switch (input.visibility_mode) {
case CanvasCursorVisibilityMode::always:
plan.visible = true;
break;
case CanvasCursorVisibilityMode::never:
plan.visible = false;
break;
case CanvasCursorVisibilityMode::small_brush:
if (!input.has_current_brush) {
return pp::foundation::Result<CanvasCursorVisibilityPlan>::failure(
pp::foundation::Status::invalid_argument("canvas cursor small-brush mode requires a current brush"));
}
if (!std::isfinite(input.brush_tip_size) || input.brush_tip_size < 0.0F) {
return pp::foundation::Result<CanvasCursorVisibilityPlan>::failure(
pp::foundation::Status::invalid_argument("canvas cursor brush size must be finite and non-negative"));
}
plan.visible = input.brush_tip_size < 10.0F;
plan.uses_brush_size = true;
break;
case CanvasCursorVisibilityMode::not_painting:
plan.visible = !input.pen_is_drawing;
plan.uses_pen_state = true;
break;
}
if (input.alt_down || input.pen_is_resizing || input.pen_is_picking) {
plan.visible = true;
plan.forced_visible_by_modifier_or_tool = true;
}
return pp::foundation::Result<CanvasCursorVisibilityPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Status execute_canvas_tool_plan( [[nodiscard]] inline pp::foundation::Status execute_canvas_tool_plan(
const CanvasToolPlan& plan, const CanvasToolPlan& plan,
CanvasToolServices& services) CanvasToolServices& services)

View File

@@ -258,6 +258,54 @@ pp::app::CanvasHotkeyKey canvas_hotkey_key(kKey key) noexcept
} }
} }
pp::app::CanvasToolMode canvas_tool_mode(kCanvasMode mode) noexcept
{
switch (mode) {
case kCanvasMode::Draw:
return pp::app::CanvasToolMode::draw;
case kCanvasMode::Erase:
return pp::app::CanvasToolMode::erase;
case kCanvasMode::Line:
return pp::app::CanvasToolMode::line;
case kCanvasMode::Camera:
return pp::app::CanvasToolMode::camera;
case kCanvasMode::Grid:
return pp::app::CanvasToolMode::grid;
case kCanvasMode::Copy:
return pp::app::CanvasToolMode::copy;
case kCanvasMode::Cut:
return pp::app::CanvasToolMode::cut;
case kCanvasMode::Fill:
return pp::app::CanvasToolMode::fill;
case kCanvasMode::MaskFree:
return pp::app::CanvasToolMode::mask_free;
case kCanvasMode::MaskLine:
return pp::app::CanvasToolMode::mask_line;
case kCanvasMode::FloodFill:
return pp::app::CanvasToolMode::flood_fill;
case kCanvasMode::COUNT:
return pp::app::CanvasToolMode::draw;
}
return pp::app::CanvasToolMode::draw;
}
pp::app::CanvasCursorVisibilityMode canvas_cursor_visibility_mode(NodeCanvas::kCursorVisibility mode) noexcept
{
switch (mode) {
case NodeCanvas::kCursorVisibility::Never:
return pp::app::CanvasCursorVisibilityMode::never;
case NodeCanvas::kCursorVisibility::SmallBrush:
return pp::app::CanvasCursorVisibilityMode::small_brush;
case NodeCanvas::kCursorVisibility::NotPainting:
return pp::app::CanvasCursorVisibilityMode::not_painting;
case NodeCanvas::kCursorVisibility::Always:
return pp::app::CanvasCursorVisibilityMode::always;
}
return pp::app::CanvasCursorVisibilityMode::never;
}
pp::app::CanvasHotkeyState canvas_hotkey_state(bool mouse_focused, int touch_finger_count = 0) noexcept pp::app::CanvasHotkeyState canvas_hotkey_state(bool mouse_focused, int touch_finger_count = 0) noexcept
{ {
pp::app::CanvasHotkeyState state; pp::app::CanvasHotkeyState state;
@@ -961,24 +1009,24 @@ void NodeCanvas::set_cursor_visibility(kCursorVisibility mode)
void NodeCanvas::update_cursor() void NodeCanvas::update_cursor()
{ {
bool visible = true; auto* pen_mode = m_canvas->get_mode<CanvasModePen>();
if (m_canvas->m_current_mode == kCanvasMode::Draw || const auto plan = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
m_canvas->m_current_mode == kCanvasMode::Erase) .mode = canvas_tool_mode(m_canvas->m_current_mode),
{ .visibility_mode = canvas_cursor_visibility_mode(m_cursor_visibility),
if (m_cursor_visibility == kCursorVisibility::Always) .has_current_brush = m_canvas->m_current_brush != nullptr,
visible = true; .brush_tip_size = m_canvas->m_current_brush ? m_canvas->m_current_brush->m_tip_size : 0.0F,
if (m_cursor_visibility == kCursorVisibility::Never) .pen_is_drawing = pen_mode && pen_mode->m_drawing,
visible = false; .alt_down = App::I && App::I->keys[(int)kKey::KeyAlt],
if (m_cursor_visibility == kCursorVisibility::SmallBrush) .pen_is_resizing = pen_mode && pen_mode->m_resizing,
visible = m_canvas->m_current_brush->m_tip_size < 10; .pen_is_picking = pen_mode && pen_mode->m_picking,
if (m_cursor_visibility == kCursorVisibility::NotPainting) });
visible = !m_canvas->get_mode<CanvasModePen>()->m_drawing; if (!plan) {
if (App::I->keys[(int)kKey::KeyAlt] || LOG("Canvas cursor visibility planning failed: %s", plan.status().message);
m_canvas->get_mode<CanvasModePen>()->m_resizing || App::I->show_cursor();
m_canvas->get_mode<CanvasModePen>()->m_picking) return;
visible = true;
} }
visible ? App::I->show_cursor() : App::I->hide_cursor();
plan.value().visible ? App::I->show_cursor() : App::I->hide_cursor();
} }
void NodeCanvas::on_tick(float dt) void NodeCanvas::on_tick(float dt)

View File

@@ -1628,6 +1628,42 @@ if(TARGET pano_cli)
LABELS "app;ui;integration;desktop-fast;fuzz" LABELS "app;ui;integration;desktop-fast;fuzz"
WILL_FAIL TRUE) WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_canvas_cursor_small_brush_smoke
COMMAND pano_cli plan-canvas-cursor --mode draw --visibility small-brush --brush-size 9.5)
set_tests_properties(pano_cli_plan_canvas_cursor_small_brush_smoke PROPERTIES
LABELS "app;ui;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-cursor\".*\"mode\":\"draw\".*\"visibility\":\"small-brush\".*\"visible\":true.*\"paintMode\":true.*\"usesBrushSize\":true")
add_test(NAME pano_cli_plan_canvas_cursor_not_painting_hidden_smoke
COMMAND pano_cli plan-canvas-cursor --mode erase --visibility not-painting --drawing)
set_tests_properties(pano_cli_plan_canvas_cursor_not_painting_hidden_smoke PROPERTIES
LABELS "app;ui;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-cursor\".*\"mode\":\"erase\".*\"visibility\":\"not-painting\".*\"drawing\":true.*\"visible\":false.*\"usesPenState\":true")
add_test(NAME pano_cli_plan_canvas_cursor_alt_forces_visible_smoke
COMMAND pano_cli plan-canvas-cursor --mode draw --visibility never --alt)
set_tests_properties(pano_cli_plan_canvas_cursor_alt_forces_visible_smoke PROPERTIES
LABELS "app;ui;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-cursor\".*\"visibility\":\"never\".*\"visible\":true.*\"forcedVisibleByModifierOrTool\":true")
add_test(NAME pano_cli_plan_canvas_cursor_non_paint_smoke
COMMAND pano_cli plan-canvas-cursor --mode camera --visibility never)
set_tests_properties(pano_cli_plan_canvas_cursor_non_paint_smoke PROPERTIES
LABELS "app;ui;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-cursor\".*\"mode\":\"camera\".*\"visible\":true.*\"paintMode\":false")
add_test(NAME pano_cli_plan_canvas_cursor_rejects_missing_brush
COMMAND pano_cli plan-canvas-cursor --visibility small-brush --no-brush)
set_tests_properties(pano_cli_plan_canvas_cursor_rejects_missing_brush PROPERTIES
LABELS "app;ui;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_canvas_cursor_rejects_bad_size
COMMAND pano_cli plan-canvas-cursor --visibility small-brush --bad-size)
set_tests_properties(pano_cli_plan_canvas_cursor_rejects_bad_size PROPERTIES
LABELS "app;ui;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_grid_operation_pick_smoke add_test(NAME pano_cli_plan_grid_operation_pick_smoke
COMMAND pano_cli plan-grid-operation --kind pick) COMMAND pano_cli plan-grid-operation --kind pick)
set_tests_properties(pano_cli_plan_grid_operation_pick_smoke PROPERTIES set_tests_properties(pano_cli_plan_grid_operation_pick_smoke PROPERTIES

View File

@@ -1,6 +1,7 @@
#include "app_core/canvas_tool_ui.h" #include "app_core/canvas_tool_ui.h"
#include "test_harness.h" #include "test_harness.h"
#include <cmath>
#include <string> #include <string>
namespace { namespace {
@@ -128,6 +129,96 @@ void button_state_tracks_active_mode_and_toggles(pp::tests::Harness& harness)
PP_EXPECT(harness, !bucket.mask_line_active); PP_EXPECT(harness, !bucket.mask_line_active);
} }
void cursor_visibility_projects_canvas_state(pp::tests::Harness& harness)
{
const auto non_paint = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
.mode = pp::app::CanvasToolMode::camera,
.visibility_mode = pp::app::CanvasCursorVisibilityMode::never,
});
PP_EXPECT(harness, non_paint);
if (non_paint) {
PP_EXPECT(harness, non_paint.value().visible);
PP_EXPECT(harness, !non_paint.value().paint_mode);
}
const auto small = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
.mode = pp::app::CanvasToolMode::draw,
.visibility_mode = pp::app::CanvasCursorVisibilityMode::small_brush,
.has_current_brush = true,
.brush_tip_size = 9.5F,
});
PP_EXPECT(harness, small);
if (small) {
PP_EXPECT(harness, small.value().visible);
PP_EXPECT(harness, small.value().paint_mode);
PP_EXPECT(harness, small.value().uses_brush_size);
}
const auto large = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
.mode = pp::app::CanvasToolMode::erase,
.visibility_mode = pp::app::CanvasCursorVisibilityMode::small_brush,
.has_current_brush = true,
.brush_tip_size = 10.0F,
});
PP_EXPECT(harness, large);
if (large) {
PP_EXPECT(harness, !large.value().visible);
PP_EXPECT(harness, large.value().uses_brush_size);
}
const auto painting = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
.mode = pp::app::CanvasToolMode::draw,
.visibility_mode = pp::app::CanvasCursorVisibilityMode::not_painting,
.pen_is_drawing = true,
});
PP_EXPECT(harness, painting);
if (painting) {
PP_EXPECT(harness, !painting.value().visible);
PP_EXPECT(harness, painting.value().uses_pen_state);
}
}
void cursor_visibility_forces_visible_for_modifier_or_tool(pp::tests::Harness& harness)
{
const auto alt = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
.mode = pp::app::CanvasToolMode::draw,
.visibility_mode = pp::app::CanvasCursorVisibilityMode::never,
.alt_down = true,
});
PP_EXPECT(harness, alt);
if (alt) {
PP_EXPECT(harness, alt.value().visible);
PP_EXPECT(harness, alt.value().forced_visible_by_modifier_or_tool);
}
const auto picking = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
.mode = pp::app::CanvasToolMode::erase,
.visibility_mode = pp::app::CanvasCursorVisibilityMode::not_painting,
.pen_is_drawing = true,
.pen_is_picking = true,
});
PP_EXPECT(harness, picking);
if (picking) {
PP_EXPECT(harness, picking.value().visible);
PP_EXPECT(harness, picking.value().forced_visible_by_modifier_or_tool);
}
}
void cursor_visibility_rejects_invalid_brush_state(pp::tests::Harness& harness)
{
PP_EXPECT(harness, !pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
.mode = pp::app::CanvasToolMode::draw,
.visibility_mode = pp::app::CanvasCursorVisibilityMode::small_brush,
.has_current_brush = false,
}));
PP_EXPECT(harness, !pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
.mode = pp::app::CanvasToolMode::draw,
.visibility_mode = pp::app::CanvasCursorVisibilityMode::small_brush,
.has_current_brush = true,
.brush_tip_size = std::nanf(""),
}));
}
void executor_dispatches_tool_actions(pp::tests::Harness& harness) void executor_dispatches_tool_actions(pp::tests::Harness& harness)
{ {
FakeCanvasToolServices services; FakeCanvasToolServices services;
@@ -205,6 +296,9 @@ int main()
harness.run("transform tools plan copy and cut actions", transform_tools_plan_copy_and_cut_actions); harness.run("transform tools plan copy and cut actions", transform_tools_plan_copy_and_cut_actions);
harness.run("pick and touch lock toggle state", pick_and_touch_lock_toggle_state); harness.run("pick and touch lock toggle state", pick_and_touch_lock_toggle_state);
harness.run("button state tracks active mode and toggles", button_state_tracks_active_mode_and_toggles); harness.run("button state tracks active mode and toggles", button_state_tracks_active_mode_and_toggles);
harness.run("cursor visibility projects canvas state", cursor_visibility_projects_canvas_state);
harness.run("cursor visibility forces visible for modifier or tool", cursor_visibility_forces_visible_for_modifier_or_tool);
harness.run("cursor visibility rejects invalid brush state", cursor_visibility_rejects_invalid_brush_state);
harness.run("executor dispatches tool actions", executor_dispatches_tool_actions); harness.run("executor dispatches tool actions", executor_dispatches_tool_actions);
harness.run("executor no-ops pick when not in draw mode", executor_no_ops_pick_when_not_in_draw_mode); harness.run("executor no-ops pick when not in draw mode", executor_no_ops_pick_when_not_in_draw_mode);
harness.run("executor rejects malformed plans", executor_rejects_malformed_plans); harness.run("executor rejects malformed plans", executor_rejects_malformed_plans);

View File

@@ -515,6 +515,18 @@ struct PlanCanvasToolStateArgs {
bool touch_lock = false; bool touch_lock = false;
}; };
struct PlanCanvasCursorArgs {
std::string mode = "draw";
std::string visibility = "small-brush";
bool has_brush = true;
float brush_size = 9.5F;
bool drawing = false;
bool alt = false;
bool resizing = false;
bool picking = false;
bool bad_size = false;
};
struct PlanQuickOperationArgs { struct PlanQuickOperationArgs {
std::string kind = "brush"; std::string kind = "brush";
int current_index = 0; int current_index = 0;
@@ -1456,6 +1468,22 @@ const char* canvas_tool_transform_action_name(pp::app::CanvasToolTransformAction
return "none"; return "none";
} }
const char* canvas_cursor_visibility_mode_name(pp::app::CanvasCursorVisibilityMode mode) noexcept
{
switch (mode) {
case pp::app::CanvasCursorVisibilityMode::never:
return "never";
case pp::app::CanvasCursorVisibilityMode::small_brush:
return "small-brush";
case pp::app::CanvasCursorVisibilityMode::not_painting:
return "not-painting";
case pp::app::CanvasCursorVisibilityMode::always:
return "always";
}
return "never";
}
const char* canvas_hotkey_event_name(pp::app::CanvasHotkeyEvent event) noexcept const char* canvas_hotkey_event_name(pp::app::CanvasHotkeyEvent event) noexcept
{ {
switch (event) { switch (event) {
@@ -2003,6 +2031,7 @@ void print_help()
<< " plan-canvas-hotkey --event key-down|key-up|touch-tap --key e|z|s|tab|alt|android-back|bracket-left|bracket-right [--ctrl] [--shift] [--mouse-focus] [--undo-count N] [--redo-count N] [--touch-fingers N]\n" << " plan-canvas-hotkey --event key-down|key-up|touch-tap --key e|z|s|tab|alt|android-back|bracket-left|bracket-right [--ctrl] [--shift] [--mouse-focus] [--undo-count N] [--redo-count N] [--touch-fingers N]\n"
<< " plan-canvas-tool --kind draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket|pick|touch-lock [--current-mode-draw]\n" << " plan-canvas-tool --kind draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket|pick|touch-lock [--current-mode-draw]\n"
<< " plan-canvas-tool-state [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--picking] [--touch-lock]\n" << " plan-canvas-tool-state [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--picking] [--touch-lock]\n"
<< " plan-canvas-cursor [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--visibility never|small-brush|not-painting|always] [--brush-size N] [--no-brush] [--drawing] [--alt] [--resizing] [--picking] [--bad-size]\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-history-operation --kind undo|redo|clear [--undo-count N] [--redo-count N] [--memory-bytes N]\n" << " 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-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"
@@ -6436,6 +6465,30 @@ pp::foundation::Result<pp::app::CanvasToolMode> parse_canvas_tool_mode(std::stri
pp::foundation::Status::invalid_argument("unknown canvas tool mode")); pp::foundation::Status::invalid_argument("unknown canvas tool mode"));
} }
pp::foundation::Result<pp::app::CanvasCursorVisibilityMode> parse_canvas_cursor_visibility_mode(
std::string_view mode)
{
if (mode == "never") {
return pp::foundation::Result<pp::app::CanvasCursorVisibilityMode>::success(
pp::app::CanvasCursorVisibilityMode::never);
}
if (mode == "small-brush") {
return pp::foundation::Result<pp::app::CanvasCursorVisibilityMode>::success(
pp::app::CanvasCursorVisibilityMode::small_brush);
}
if (mode == "not-painting") {
return pp::foundation::Result<pp::app::CanvasCursorVisibilityMode>::success(
pp::app::CanvasCursorVisibilityMode::not_painting);
}
if (mode == "always") {
return pp::foundation::Result<pp::app::CanvasCursorVisibilityMode>::success(
pp::app::CanvasCursorVisibilityMode::always);
}
return pp::foundation::Result<pp::app::CanvasCursorVisibilityMode>::failure(
pp::foundation::Status::invalid_argument("unknown canvas cursor visibility mode"));
}
int plan_canvas_tool(int argc, char** argv) int plan_canvas_tool(int argc, char** argv)
{ {
PlanCanvasToolArgs args; PlanCanvasToolArgs args;
@@ -6693,6 +6746,105 @@ int plan_canvas_tool_state(int argc, char** argv)
return 0; return 0;
} }
pp::foundation::Status parse_plan_canvas_cursor_args(
int argc,
char** argv,
PlanCanvasCursorArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--mode" || key == "--visibility") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
if (key == "--mode") {
args.mode = argv[++i];
} else {
args.visibility = argv[++i];
}
} else if (key == "--brush-size") {
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();
}
args.brush_size = value.value();
} else if (key == "--no-brush") {
args.has_brush = false;
} else if (key == "--drawing") {
args.drawing = true;
} else if (key == "--alt") {
args.alt = true;
} else if (key == "--resizing") {
args.resizing = true;
} else if (key == "--picking") {
args.picking = true;
} else if (key == "--bad-size") {
args.bad_size = true;
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
int plan_canvas_cursor(int argc, char** argv)
{
PlanCanvasCursorArgs args;
const auto status = parse_plan_canvas_cursor_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-canvas-cursor", status.message);
return 2;
}
const auto mode = parse_canvas_tool_mode(args.mode);
if (!mode) {
print_error("plan-canvas-cursor", mode.status().message);
return 2;
}
const auto visibility = parse_canvas_cursor_visibility_mode(args.visibility);
if (!visibility) {
print_error("plan-canvas-cursor", visibility.status().message);
return 2;
}
const auto plan = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
.mode = mode.value(),
.visibility_mode = visibility.value(),
.has_current_brush = args.has_brush,
.brush_tip_size = args.bad_size ? std::nanf("") : args.brush_size,
.pen_is_drawing = args.drawing,
.alt_down = args.alt,
.pen_is_resizing = args.resizing,
.pen_is_picking = args.picking,
});
if (!plan) {
print_error("plan-canvas-cursor", plan.status().message);
return 2;
}
const auto& value = plan.value();
std::cout << "{\"ok\":true,\"command\":\"plan-canvas-cursor\""
<< ",\"state\":{\"mode\":\"" << canvas_tool_mode_name(mode.value())
<< "\",\"visibility\":\"" << canvas_cursor_visibility_mode_name(visibility.value())
<< "\",\"hasBrush\":" << json_bool(args.has_brush)
<< ",\"brushSize\":" << args.brush_size
<< ",\"drawing\":" << json_bool(args.drawing)
<< ",\"alt\":" << json_bool(args.alt)
<< ",\"resizing\":" << json_bool(args.resizing)
<< ",\"picking\":" << json_bool(args.picking)
<< "},\"plan\":{\"visible\":" << json_bool(value.visible)
<< ",\"paintMode\":" << json_bool(value.paint_mode)
<< ",\"usesBrushSize\":" << json_bool(value.uses_brush_size)
<< ",\"usesPenState\":" << json_bool(value.uses_pen_state)
<< ",\"forcedVisibleByModifierOrTool\":" << json_bool(value.forced_visible_by_modifier_or_tool)
<< "}}\n";
return 0;
}
pp::foundation::Status parse_plan_grid_operation_args( pp::foundation::Status parse_plan_grid_operation_args(
int argc, int argc,
char** argv, char** argv,
@@ -9753,6 +9905,10 @@ int main(int argc, char** argv)
return plan_canvas_tool_state(argc, argv); return plan_canvas_tool_state(argc, argv);
} }
if (command == "plan-canvas-cursor") {
return plan_canvas_cursor(argc, argv);
}
if (command == "plan-grid-operation") { if (command == "plan-grid-operation") {
return plan_grid_operation(argc, argv); return plan_grid_operation(argc, argv);
} }