#pragma once #include "foundation/result.h" #include #include #include namespace pp::app { enum class CanvasToolOperation { select_mode, toggle_picking, toggle_touch_lock, }; enum class CanvasToolMode { draw, erase, line, camera, grid, copy, cut, fill, mask_free, mask_line, flood_fill, }; enum class CanvasToolTransformAction { none, copy, cut, }; enum class CanvasToolToolbarAction { select_mode, toggle_picking, toggle_touch_lock, }; enum class CanvasCursorVisibilityMode { never, small_brush, not_painting, always, }; struct CanvasToolPlan { CanvasToolOperation operation = CanvasToolOperation::select_mode; CanvasToolMode mode = CanvasToolMode::draw; CanvasToolTransformAction transform_action = CanvasToolTransformAction::none; bool selects_toolbar_button = false; bool updates_canvas_mode = false; bool toggles_picking = false; bool toggles_touch_lock = false; bool requires_draw_mode = false; bool no_op = false; }; struct CanvasToolButtonState { CanvasToolMode mode = CanvasToolMode::draw; bool pick_active = false; bool touch_lock_active = false; bool pen_active = false; bool erase_active = false; bool line_active = false; bool camera_active = false; bool grid_active = false; bool copy_active = false; bool cut_active = false; bool fill_active = false; bool mask_free_active = false; bool mask_line_active = false; bool flood_fill_active = false; }; struct CanvasToolToolbarBinding { std::string_view button_id; CanvasToolToolbarAction action = CanvasToolToolbarAction::select_mode; CanvasToolMode mode = CanvasToolMode::draw; bool custom_button = true; bool applies_default_on_init = false; }; struct CanvasToolToolbarPlan { std::array bindings {}; CanvasToolMode default_mode = CanvasToolMode::draw; }; 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 { public: virtual ~CanvasToolServices() = default; virtual void select_toolbar_button(CanvasToolMode mode) = 0; virtual void set_transform_action(CanvasToolTransformAction action) = 0; virtual void set_canvas_mode(CanvasToolMode mode) = 0; virtual void toggle_picking() = 0; virtual void toggle_touch_lock() = 0; }; [[nodiscard]] inline constexpr CanvasToolTransformAction transform_action_for_mode(CanvasToolMode mode) noexcept { if (mode == CanvasToolMode::copy) { return CanvasToolTransformAction::copy; } if (mode == CanvasToolMode::cut) { return CanvasToolTransformAction::cut; } return CanvasToolTransformAction::none; } [[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_select(CanvasToolMode mode) noexcept { CanvasToolPlan plan; plan.operation = CanvasToolOperation::select_mode; plan.mode = mode; plan.transform_action = transform_action_for_mode(mode); plan.selects_toolbar_button = true; plan.updates_canvas_mode = true; return plan; } [[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_pick_toggle(bool current_mode_is_draw) noexcept { CanvasToolPlan plan; plan.operation = CanvasToolOperation::toggle_picking; plan.mode = CanvasToolMode::draw; plan.requires_draw_mode = true; plan.toggles_picking = current_mode_is_draw; plan.no_op = !current_mode_is_draw; return plan; } [[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_touch_lock_toggle() noexcept { CanvasToolPlan plan; plan.operation = CanvasToolOperation::toggle_touch_lock; plan.toggles_touch_lock = true; return plan; } [[nodiscard]] inline constexpr CanvasToolToolbarPlan plan_canvas_tool_toolbar() noexcept { return { std::array { CanvasToolToolbarBinding { "btn-pen", CanvasToolToolbarAction::select_mode, CanvasToolMode::draw, true, true }, CanvasToolToolbarBinding { "btn-pick", CanvasToolToolbarAction::toggle_picking, CanvasToolMode::draw, true, false }, CanvasToolToolbarBinding { "btn-touchlock", CanvasToolToolbarAction::toggle_touch_lock, CanvasToolMode::draw, true, false }, CanvasToolToolbarBinding { "btn-erase", CanvasToolToolbarAction::select_mode, CanvasToolMode::erase, true, false }, CanvasToolToolbarBinding { "btn-line", CanvasToolToolbarAction::select_mode, CanvasToolMode::line, true, false }, CanvasToolToolbarBinding { "btn-cam", CanvasToolToolbarAction::select_mode, CanvasToolMode::camera, false, false }, CanvasToolToolbarBinding { "btn-grid", CanvasToolToolbarAction::select_mode, CanvasToolMode::grid, false, false }, CanvasToolToolbarBinding { "btn-copy", CanvasToolToolbarAction::select_mode, CanvasToolMode::copy, false, false }, CanvasToolToolbarBinding { "btn-cut", CanvasToolToolbarAction::select_mode, CanvasToolMode::cut, false, false }, CanvasToolToolbarBinding { "btn-fill", CanvasToolToolbarAction::select_mode, CanvasToolMode::fill, false, false }, CanvasToolToolbarBinding { "btn-mask-free", CanvasToolToolbarAction::select_mode, CanvasToolMode::mask_free, true, false }, CanvasToolToolbarBinding { "btn-mask-line", CanvasToolToolbarAction::select_mode, CanvasToolMode::mask_line, true, false }, CanvasToolToolbarBinding { "btn-bucket", CanvasToolToolbarAction::select_mode, CanvasToolMode::flood_fill, true, false }, }, CanvasToolMode::draw, }; } [[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_toolbar_binding_action( const CanvasToolToolbarBinding& binding, bool current_mode_is_draw) noexcept { switch (binding.action) { case CanvasToolToolbarAction::select_mode: return plan_canvas_tool_select(binding.mode); case CanvasToolToolbarAction::toggle_picking: return plan_canvas_tool_pick_toggle(current_mode_is_draw); case CanvasToolToolbarAction::toggle_touch_lock: return plan_canvas_tool_touch_lock_toggle(); } return plan_canvas_tool_select(CanvasToolMode::draw); } [[nodiscard]] inline constexpr CanvasToolButtonState plan_canvas_tool_button_state( CanvasToolMode mode, bool picking, bool touch_lock) noexcept { CanvasToolButtonState state; state.mode = mode; state.pick_active = mode == CanvasToolMode::draw && picking; state.touch_lock_active = touch_lock; state.pen_active = mode == CanvasToolMode::draw; state.erase_active = mode == CanvasToolMode::erase; state.line_active = mode == CanvasToolMode::line; state.camera_active = mode == CanvasToolMode::camera; state.grid_active = mode == CanvasToolMode::grid; state.copy_active = mode == CanvasToolMode::copy; state.cut_active = mode == CanvasToolMode::cut; state.fill_active = mode == CanvasToolMode::fill; state.mask_free_active = mode == CanvasToolMode::mask_free; state.mask_line_active = mode == CanvasToolMode::mask_line; state.flood_fill_active = mode == CanvasToolMode::flood_fill; 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 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::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::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::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::success(plan); } [[nodiscard]] inline pp::foundation::Status execute_canvas_tool_plan( const CanvasToolPlan& plan, CanvasToolServices& services) { switch (plan.operation) { case CanvasToolOperation::select_mode: if (!plan.selects_toolbar_button || !plan.updates_canvas_mode) { return pp::foundation::Status::invalid_argument("canvas tool select plan must select toolbar and update mode"); } if (plan.transform_action != transform_action_for_mode(plan.mode)) { return pp::foundation::Status::invalid_argument("canvas tool select plan has mismatched transform action"); } services.select_toolbar_button(plan.mode); if (plan.transform_action != CanvasToolTransformAction::none) { services.set_transform_action(plan.transform_action); } services.set_canvas_mode(plan.mode); return pp::foundation::Status::success(); case CanvasToolOperation::toggle_picking: if (!plan.requires_draw_mode) { return pp::foundation::Status::invalid_argument("canvas pick plan must require draw mode"); } if (plan.no_op) { return pp::foundation::Status::success(); } if (!plan.toggles_picking) { return pp::foundation::Status::invalid_argument("canvas pick plan must toggle picking or be a no-op"); } services.toggle_picking(); return pp::foundation::Status::success(); case CanvasToolOperation::toggle_touch_lock: if (!plan.toggles_touch_lock || plan.no_op) { return pp::foundation::Status::invalid_argument("canvas touch-lock plan must toggle touch lock"); } services.toggle_touch_lock(); return pp::foundation::Status::success(); } return pp::foundation::Status::invalid_argument("unknown canvas tool operation"); } } // namespace pp::app