#pragma once #include "foundation/result.h" #include #include #include #include #include #include namespace pp::app { inline constexpr std::size_t document_layer_name_max_length = 128; inline constexpr int document_layer_legacy_blend_mode_count = 5; enum class DocumentLayerRenameAction { no_op_same_name, rename_and_record_undo, }; enum class DocumentLayerOperation { add, duplicate, select, reorder, remove, set_opacity, set_visibility, set_alpha_lock, set_blend_mode, set_highlight, }; enum class DocumentLayerMenuCommand { clear, rename, merge_down, }; enum class DocumentLayerMenuAction { clear_current_layer, show_rename_dialog, merge_with_lower_layer, show_merge_animated_not_supported, no_op_select_layer, no_op_select_upper_layer, }; struct DocumentLayerRenamePlan { std::string old_name; std::string new_name; DocumentLayerRenameAction action = DocumentLayerRenameAction::no_op_same_name; }; struct DocumentLayerOperationPlan { DocumentLayerOperation operation = DocumentLayerOperation::select; int index = 0; int from_index = 0; int to_index = 0; int insert_index = 0; int source_index = 0; std::string name; float opacity = 1.0F; bool flag = false; int blend_mode = 0; bool mutates_document = false; bool marks_unsaved = false; bool reloads_animation_layers = false; bool updates_title = false; }; struct DocumentLayerMenuPlan { DocumentLayerMenuCommand command = DocumentLayerMenuCommand::clear; DocumentLayerMenuAction action = DocumentLayerMenuAction::clear_current_layer; std::string label; int from_index = 0; int to_index = 0; }; struct DocumentLayerMergePlan { int from_index = 0; int to_index = 0; bool create_history = true; }; class DocumentLayerMenuServices { public: virtual ~DocumentLayerMenuServices() = default; virtual void clear_current_layer() = 0; virtual void show_rename_dialog() = 0; virtual void merge_with_lower_layer(int from_index, int to_index) = 0; virtual void show_merge_animated_not_supported() = 0; }; class DocumentLayerRenameServices { public: virtual ~DocumentLayerRenameServices() = default; virtual void rename_layer(std::string_view old_name, std::string_view new_name) = 0; virtual void finish_layer_rename() = 0; }; class DocumentLayerOperationServices { public: virtual ~DocumentLayerOperationServices() = default; virtual void add_layer(std::string_view name, int insert_index) = 0; virtual void duplicate_layer(int source_index, int insert_index) = 0; virtual void select_layer(int index) = 0; virtual void reorder_layer(int from_index, int to_index) = 0; virtual void remove_layer(int index) = 0; virtual void set_layer_opacity(int index, float opacity) = 0; virtual void set_layer_visibility(int index, bool visible) = 0; virtual void set_layer_alpha_lock(int index, bool locked) = 0; virtual void set_layer_blend_mode(int index, int blend_mode) = 0; virtual void set_layer_highlight(int index, bool highlighted) = 0; virtual void mark_unsaved() = 0; virtual void reload_animation_layers() = 0; virtual void update_title() = 0; }; class DocumentLayerMergeServices { public: virtual ~DocumentLayerMergeServices() = default; virtual void merge_layers(int from_index, int to_index, bool create_history) = 0; }; [[nodiscard]] inline pp::foundation::Status validate_layer_index( int layer_count, int index) noexcept { if (layer_count <= 0) { return pp::foundation::Status::invalid_argument("document must contain at least one layer"); } if (index < 0 || index >= layer_count) { return pp::foundation::Status::out_of_range("layer index is outside the document"); } return pp::foundation::Status::success(); } [[nodiscard]] inline pp::foundation::Status validate_layer_insert_index( int layer_count, int index) noexcept { if (layer_count < 0) { return pp::foundation::Status::invalid_argument("layer count must not be negative"); } if (index < 0 || index > layer_count) { return pp::foundation::Status::out_of_range("layer insert index is outside the document"); } return pp::foundation::Status::success(); } [[nodiscard]] inline pp::foundation::Result plan_document_layer_rename( std::string_view old_name, std::string_view requested_name) { if (requested_name.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("layer name must not be empty")); } if (requested_name.size() > document_layer_name_max_length) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("layer name length exceeds the configured limit")); } DocumentLayerRenamePlan plan; plan.old_name = std::string(old_name); plan.new_name = std::string(requested_name); plan.action = old_name == requested_name ? DocumentLayerRenameAction::no_op_same_name : DocumentLayerRenameAction::rename_and_record_undo; return pp::foundation::Result::success(std::move(plan)); } [[nodiscard]] inline pp::foundation::Result plan_document_layer_add( int layer_count, int insert_index, std::string_view name) { const auto index_status = validate_layer_insert_index(layer_count, insert_index); if (!index_status.ok()) { return pp::foundation::Result::failure(index_status); } const auto rename = plan_document_layer_rename({}, name); if (!rename) { return pp::foundation::Result::failure(rename.status()); } DocumentLayerOperationPlan plan; plan.operation = DocumentLayerOperation::add; plan.insert_index = insert_index; plan.name = std::string(name); plan.mutates_document = true; plan.marks_unsaved = true; plan.reloads_animation_layers = true; plan.updates_title = true; return pp::foundation::Result::success(std::move(plan)); } [[nodiscard]] inline pp::foundation::Result plan_document_layer_duplicate( int layer_count, int source_index) { const auto index_status = validate_layer_index(layer_count, source_index); if (!index_status.ok()) { return pp::foundation::Result::failure(index_status); } DocumentLayerOperationPlan plan; plan.operation = DocumentLayerOperation::duplicate; plan.source_index = source_index; plan.insert_index = source_index + 1; plan.mutates_document = true; plan.marks_unsaved = true; plan.reloads_animation_layers = true; plan.updates_title = true; return pp::foundation::Result::success(plan); } [[nodiscard]] inline pp::foundation::Result plan_document_layer_select( int layer_count, int index) { const auto index_status = validate_layer_index(layer_count, index); if (!index_status.ok()) { return pp::foundation::Result::failure(index_status); } DocumentLayerOperationPlan plan; plan.operation = DocumentLayerOperation::select; plan.index = index; plan.reloads_animation_layers = true; return pp::foundation::Result::success(plan); } [[nodiscard]] inline pp::foundation::Result plan_document_layer_reorder( int layer_count, int from_index, int to_index) { auto index_status = validate_layer_index(layer_count, from_index); if (!index_status.ok()) { return pp::foundation::Result::failure(index_status); } index_status = validate_layer_index(layer_count, to_index); if (!index_status.ok()) { return pp::foundation::Result::failure(index_status); } DocumentLayerOperationPlan plan; plan.operation = DocumentLayerOperation::reorder; plan.from_index = from_index; plan.to_index = to_index; plan.mutates_document = from_index != to_index; plan.marks_unsaved = plan.mutates_document; plan.reloads_animation_layers = plan.mutates_document; plan.updates_title = plan.mutates_document; return pp::foundation::Result::success(plan); } [[nodiscard]] inline pp::foundation::Result plan_document_layer_remove( int layer_count, int index) { const auto index_status = validate_layer_index(layer_count, index); if (!index_status.ok()) { return pp::foundation::Result::failure(index_status); } if (layer_count <= 1) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document must keep at least one layer")); } DocumentLayerOperationPlan plan; plan.operation = DocumentLayerOperation::remove; plan.index = index; plan.mutates_document = true; plan.marks_unsaved = true; plan.reloads_animation_layers = true; plan.updates_title = true; return pp::foundation::Result::success(plan); } [[nodiscard]] inline pp::foundation::Result plan_document_layer_opacity( int layer_count, int index, float opacity) { const auto index_status = validate_layer_index(layer_count, index); if (!index_status.ok()) { return pp::foundation::Result::failure(index_status); } if (!std::isfinite(opacity) || opacity < 0.0F || opacity > 1.0F) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1")); } DocumentLayerOperationPlan plan; plan.operation = DocumentLayerOperation::set_opacity; plan.index = index; plan.opacity = opacity; plan.mutates_document = true; plan.marks_unsaved = true; plan.updates_title = true; return pp::foundation::Result::success(plan); } [[nodiscard]] inline pp::foundation::Result plan_document_layer_visibility( int layer_count, int index, bool visible) { const auto index_status = validate_layer_index(layer_count, index); if (!index_status.ok()) { return pp::foundation::Result::failure(index_status); } DocumentLayerOperationPlan plan; plan.operation = DocumentLayerOperation::set_visibility; plan.index = index; plan.flag = visible; plan.mutates_document = true; plan.marks_unsaved = true; plan.reloads_animation_layers = true; plan.updates_title = true; return pp::foundation::Result::success(plan); } [[nodiscard]] inline pp::foundation::Result plan_document_layer_alpha_lock( int layer_count, int index, bool locked) { const auto index_status = validate_layer_index(layer_count, index); if (!index_status.ok()) { return pp::foundation::Result::failure(index_status); } DocumentLayerOperationPlan plan; plan.operation = DocumentLayerOperation::set_alpha_lock; plan.index = index; plan.flag = locked; plan.mutates_document = true; plan.marks_unsaved = true; plan.updates_title = true; return pp::foundation::Result::success(plan); } [[nodiscard]] inline pp::foundation::Result plan_document_layer_blend_mode( int layer_count, int index, int blend_mode) { const auto index_status = validate_layer_index(layer_count, index); if (!index_status.ok()) { return pp::foundation::Result::failure(index_status); } if (blend_mode < 0 || blend_mode >= document_layer_legacy_blend_mode_count) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("layer blend mode is outside the supported range")); } DocumentLayerOperationPlan plan; plan.operation = DocumentLayerOperation::set_blend_mode; plan.index = index; plan.blend_mode = blend_mode; plan.mutates_document = true; plan.marks_unsaved = true; plan.updates_title = true; return pp::foundation::Result::success(plan); } [[nodiscard]] inline pp::foundation::Result plan_document_layer_highlight( int layer_count, int index, bool highlight) { const auto index_status = validate_layer_index(layer_count, index); if (!index_status.ok()) { return pp::foundation::Result::failure(index_status); } DocumentLayerOperationPlan plan; plan.operation = DocumentLayerOperation::set_highlight; plan.index = index; plan.flag = highlight; return pp::foundation::Result::success(plan); } [[nodiscard]] inline pp::foundation::Result plan_document_layer_menu( DocumentLayerMenuCommand command, bool has_current_layer, int current_index, int animation_duration, std::string_view current_layer_name, std::string_view lower_layer_name) { if (current_index < 0) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("current layer index must not be negative")); } if (animation_duration < 0) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("animation duration must not be negative")); } DocumentLayerMenuPlan plan; plan.command = command; plan.from_index = current_index; plan.to_index = current_index > 0 ? current_index - 1 : 0; switch (command) { case DocumentLayerMenuCommand::clear: plan.action = has_current_layer ? DocumentLayerMenuAction::clear_current_layer : DocumentLayerMenuAction::no_op_select_layer; plan.label = has_current_layer ? "Clear Layer " + std::string(current_layer_name) : "Clear Layer (Select a layer)"; break; case DocumentLayerMenuCommand::rename: plan.action = has_current_layer ? DocumentLayerMenuAction::show_rename_dialog : DocumentLayerMenuAction::no_op_select_layer; plan.label = has_current_layer ? "Rename Layer " + std::string(current_layer_name) : "Rename Layer (Select a layer)"; break; case DocumentLayerMenuCommand::merge_down: if (!has_current_layer) { plan.action = DocumentLayerMenuAction::no_op_select_layer; plan.label = "Merge Layer (Select a layer)"; } else if (animation_duration > 1) { plan.action = DocumentLayerMenuAction::show_merge_animated_not_supported; plan.label = "Merge Layer (Animation not supported)"; } else if (current_index <= 0) { plan.action = DocumentLayerMenuAction::no_op_select_upper_layer; plan.label = "Merge Layer (Select upper layers)"; } else { plan.action = DocumentLayerMenuAction::merge_with_lower_layer; plan.label = "Merge with " + std::string(lower_layer_name); } break; } return pp::foundation::Result::success(std::move(plan)); } [[nodiscard]] inline pp::foundation::Result plan_document_layer_merge( int layer_count, int from_index, int to_index, int animation_duration, bool create_history = true) { auto index_status = validate_layer_index(layer_count, from_index); if (!index_status.ok()) { return pp::foundation::Result::failure(index_status); } index_status = validate_layer_index(layer_count, to_index); if (!index_status.ok()) { return pp::foundation::Result::failure(index_status); } if (animation_duration < 0) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("animation duration must not be negative")); } if (animation_duration > 1) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("animated layer merge is not supported")); } if (from_index <= to_index) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("layer merge source must be above the destination")); } DocumentLayerMergePlan plan; plan.from_index = from_index; plan.to_index = to_index; plan.create_history = create_history; return pp::foundation::Result::success(plan); } [[nodiscard]] inline pp::foundation::Status execute_document_layer_rename_plan( const DocumentLayerRenamePlan& plan, DocumentLayerRenameServices& services) { switch (plan.action) { case DocumentLayerRenameAction::no_op_same_name: services.finish_layer_rename(); return pp::foundation::Status::success(); case DocumentLayerRenameAction::rename_and_record_undo: if (plan.new_name.empty()) { return pp::foundation::Status::invalid_argument("layer rename plan must include a new name"); } if (plan.old_name == plan.new_name) { return pp::foundation::Status::invalid_argument("layer rename plan must change the name"); } services.rename_layer(plan.old_name, plan.new_name); services.finish_layer_rename(); return pp::foundation::Status::success(); } return pp::foundation::Status::invalid_argument("unknown document layer rename action"); } inline void execute_document_layer_operation_side_effects( const DocumentLayerOperationPlan& plan, DocumentLayerOperationServices& services) { if (plan.marks_unsaved) services.mark_unsaved(); if (plan.reloads_animation_layers) services.reload_animation_layers(); if (plan.updates_title) services.update_title(); } [[nodiscard]] inline pp::foundation::Status execute_document_layer_operation_plan( const DocumentLayerOperationPlan& plan, DocumentLayerOperationServices& services) { switch (plan.operation) { case DocumentLayerOperation::add: if (!plan.mutates_document || plan.name.empty()) { return pp::foundation::Status::invalid_argument("layer add plan must mutate with a name"); } services.add_layer(plan.name, plan.insert_index); break; case DocumentLayerOperation::duplicate: if (!plan.mutates_document) { return pp::foundation::Status::invalid_argument("layer duplicate plan must mutate the document"); } services.duplicate_layer(plan.source_index, plan.insert_index); break; case DocumentLayerOperation::select: services.select_layer(plan.index); break; case DocumentLayerOperation::reorder: if (plan.mutates_document) services.reorder_layer(plan.from_index, plan.to_index); break; case DocumentLayerOperation::remove: if (!plan.mutates_document) { return pp::foundation::Status::invalid_argument("layer remove plan must mutate the document"); } services.remove_layer(plan.index); break; case DocumentLayerOperation::set_opacity: if (!plan.mutates_document) { return pp::foundation::Status::invalid_argument("layer opacity plan must mutate the document"); } services.set_layer_opacity(plan.index, plan.opacity); break; case DocumentLayerOperation::set_visibility: if (!plan.mutates_document) { return pp::foundation::Status::invalid_argument("layer visibility plan must mutate the document"); } services.set_layer_visibility(plan.index, plan.flag); break; case DocumentLayerOperation::set_alpha_lock: if (!plan.mutates_document) { return pp::foundation::Status::invalid_argument("layer alpha-lock plan must mutate the document"); } services.set_layer_alpha_lock(plan.index, plan.flag); break; case DocumentLayerOperation::set_blend_mode: if (!plan.mutates_document) { return pp::foundation::Status::invalid_argument("layer blend-mode plan must mutate the document"); } services.set_layer_blend_mode(plan.index, plan.blend_mode); break; case DocumentLayerOperation::set_highlight: services.set_layer_highlight(plan.index, plan.flag); break; } execute_document_layer_operation_side_effects(plan, services); return pp::foundation::Status::success(); } [[nodiscard]] inline pp::foundation::Status execute_document_layer_merge_plan( const DocumentLayerMergePlan& plan, DocumentLayerMergeServices& services) { if (plan.from_index <= plan.to_index) { return pp::foundation::Status::invalid_argument( "layer merge source must be above the destination"); } services.merge_layers(plan.from_index, plan.to_index, plan.create_history); return pp::foundation::Status::success(); } [[nodiscard]] inline pp::foundation::Status execute_document_layer_menu_plan( const DocumentLayerMenuPlan& plan, DocumentLayerMenuServices& services) { switch (plan.action) { case DocumentLayerMenuAction::clear_current_layer: services.clear_current_layer(); return pp::foundation::Status::success(); case DocumentLayerMenuAction::show_rename_dialog: services.show_rename_dialog(); return pp::foundation::Status::success(); case DocumentLayerMenuAction::merge_with_lower_layer: services.merge_with_lower_layer(plan.from_index, plan.to_index); return pp::foundation::Status::success(); case DocumentLayerMenuAction::show_merge_animated_not_supported: services.show_merge_animated_not_supported(); return pp::foundation::Status::success(); case DocumentLayerMenuAction::no_op_select_layer: case DocumentLayerMenuAction::no_op_select_upper_layer: return pp::foundation::Status::success(); } return pp::foundation::Status::invalid_argument("unknown document layer menu action"); } }