#include "app_core/brush_ui.h" #include "test_harness.h" #include #include #include #include namespace { class FakeBrushUiServices final : public pp::app::BrushUiServices { public: void set_tip_color(float r, float g, float b, float a) override { color_sets += 1; last_r = r; last_g = g; last_b = b; last_a = a; call_order += "color;"; } void set_texture( pp::app::BrushUiTextureSlot slot, std::string_view path, std::string_view thumbnail_path) override { texture_sets += 1; last_slot = slot; last_path = std::string(path); last_thumbnail_path = std::string(thumbnail_path); call_order += "texture;"; } void replace_brush_from_preset(bool preserve_existing_color, bool load_resources) override { preset_replacements += 1; preserved_color = preserve_existing_color; loaded_resources = load_resources; call_order += "preset;"; } void refresh_brush_ui(bool update_color_ui, bool update_brush_ui) override { refreshes += 1; last_update_color_ui = update_color_ui; last_update_brush_ui = update_brush_ui; call_order += "refresh;"; } int color_sets = 0; int texture_sets = 0; int preset_replacements = 0; int refreshes = 0; float last_r = 0.0F; float last_g = 0.0F; float last_b = 0.0F; float last_a = 0.0F; pp::app::BrushUiTextureSlot last_slot = pp::app::BrushUiTextureSlot::tip; std::string last_path; std::string last_thumbnail_path; bool preserved_color = false; bool loaded_resources = false; bool last_update_color_ui = false; bool last_update_brush_ui = false; std::string call_order; }; class FakeBrushTextureListServices final : public pp::app::BrushTextureListServices { public: pp::foundation::Status add_texture_from_source( std::string_view source_path, std::string_view high_path, std::string_view thumbnail_path, std::string_view brush_name, bool converts_brush_alpha) override { if (fail_add) { call_order += "add-failed;"; return pp::foundation::Status::invalid_argument("fake add failure"); } adds += 1; last_source_path = std::string(source_path); last_high_path = std::string(high_path); last_thumbnail_path = std::string(thumbnail_path); last_brush_name = std::string(brush_name); last_converts_brush_alpha = converts_brush_alpha; call_order += "add;"; return pp::foundation::Status::success(); } void remove_texture(int index, bool delete_texture_files) override { removes += 1; last_index = index; last_deletes_texture_files = delete_texture_files; call_order += "remove;"; } void move_texture(int from_index, int to_index) override { moves += 1; last_index = from_index; last_target_index = to_index; call_order += "move;"; } void select_texture(int index) override { selections += 1; last_target_index = index; call_order += "select;"; } void save_texture_list() override { saves += 1; call_order += "save;"; } int adds = 0; int removes = 0; int moves = 0; int selections = 0; int saves = 0; int last_index = -1; int last_target_index = -1; std::string last_source_path; std::string last_high_path; std::string last_thumbnail_path; std::string last_brush_name; bool last_converts_brush_alpha = false; bool last_deletes_texture_files = false; bool fail_add = false; std::string call_order; }; class FakeBrushPresetListServices final : public pp::app::BrushPresetListServices { public: pp::foundation::Status add_current_brush_preset(int target_index) override { if (fail_add) { call_order += "add-failed;"; return pp::foundation::Status::invalid_argument("fake preset add failure"); } adds += 1; last_target_index = target_index; call_order += "add;"; return pp::foundation::Status::success(); } void remove_brush_preset( int current_index, int target_index, bool selects_target, bool clears_selection) override { removes += 1; last_index = current_index; last_target_index = target_index; last_selects_target = selects_target; last_remove_clears_selection = clears_selection; call_order += "remove;"; } void move_brush_preset(int from_index, int to_index) override { moves += 1; last_index = from_index; last_target_index = to_index; call_order += "move;"; } void select_brush_preset(int index, bool notify_brush_changed) override { selections += 1; last_target_index = index; last_notifies_brush_changed = notify_brush_changed; call_order += "select;"; } void clear_brush_presets(bool clears_selection) override { clears += 1; last_clear_clears_selection = clears_selection; call_order += "clear;"; } void update_preset_empty_notification() override { notification_updates += 1; call_order += "notification;"; } void save_preset_list() override { saves += 1; call_order += "save;"; } int adds = 0; int removes = 0; int moves = 0; int selections = 0; int clears = 0; int notification_updates = 0; int saves = 0; int last_index = -1; int last_target_index = -1; bool last_selects_target = false; bool last_remove_clears_selection = false; bool last_clear_clears_selection = false; bool last_notifies_brush_changed = false; bool fail_add = false; std::string call_order; }; class FakeBrushStrokeControlServices final : public pp::app::BrushStrokeControlServices { public: void set_float_setting(pp::app::BrushStrokeFloatSetting setting, float value) override { float_sets += 1; last_float_setting = setting; last_float_value = value; call_order += "float;"; } void set_bool_setting(pp::app::BrushStrokeBoolSetting setting, bool value) override { bool_sets += 1; last_bool_setting = setting; last_bool_value = value; call_order += "bool;"; } void set_blend_mode(pp::app::BrushStrokeBlendSetting setting, int blend_mode) override { blend_sets += 1; last_blend_setting = setting; last_blend_mode = blend_mode; call_order += "blend;"; } void reset_tip_aspect(float value) override { tip_aspect_resets += 1; last_float_value = value; call_order += "tip-aspect;"; } void reset_default_brush() override { default_resets += 1; call_order += "default;"; } void update_stroke_controls() override { control_updates += 1; call_order += "controls;"; } void refresh_stroke_preview() override { preview_refreshes += 1; call_order += "preview;"; } void notify_stroke_changed() override { stroke_notifications += 1; call_order += "notify;"; } int float_sets = 0; int bool_sets = 0; int blend_sets = 0; int tip_aspect_resets = 0; int default_resets = 0; int control_updates = 0; int preview_refreshes = 0; int stroke_notifications = 0; pp::app::BrushStrokeFloatSetting last_float_setting = pp::app::BrushStrokeFloatSetting::tip_size; pp::app::BrushStrokeBoolSetting last_bool_setting = pp::app::BrushStrokeBoolSetting::tip_angle_init; pp::app::BrushStrokeBlendSetting last_blend_setting = pp::app::BrushStrokeBlendSetting::tip; float last_float_value = 0.0F; bool last_bool_value = false; int last_blend_mode = 0; std::string call_order; }; void color_plan_validates_all_channels(pp::tests::Harness& harness) { const auto plan = pp::app::plan_brush_ui_color(0.25F, 0.5F, 0.75F, 1.0F); PP_EXPECT(harness, plan); if (plan) { PP_EXPECT(harness, plan.value().operation == pp::app::BrushUiOperation::set_tip_color); PP_EXPECT(harness, plan.value().r == 0.25F); PP_EXPECT(harness, plan.value().g == 0.5F); PP_EXPECT(harness, plan.value().b == 0.75F); PP_EXPECT(harness, plan.value().a == 1.0F); PP_EXPECT(harness, plan.value().mutates_brush); PP_EXPECT(harness, plan.value().update_color_ui); PP_EXPECT(harness, !plan.value().update_brush_ui); } PP_EXPECT(harness, !pp::app::plan_brush_ui_color(-0.01F, 0.5F, 0.5F, 1.0F)); PP_EXPECT(harness, !pp::app::plan_brush_ui_color(0.5F, 1.01F, 0.5F, 1.0F)); PP_EXPECT(harness, !pp::app::plan_brush_ui_color(0.5F, 0.5F, std::nanf(""), 1.0F)); } void texture_plan_validates_path_and_slot(pp::tests::Harness& harness) { const auto plan = pp::app::plan_brush_ui_texture( pp::app::BrushUiTextureSlot::pattern, "data/patterns/noise.png", "data/patterns/thumbs/noise.png"); PP_EXPECT(harness, plan); if (plan) { PP_EXPECT(harness, plan.value().operation == pp::app::BrushUiOperation::set_texture); PP_EXPECT(harness, plan.value().texture_slot == pp::app::BrushUiTextureSlot::pattern); PP_EXPECT(harness, plan.value().path == "data/patterns/noise.png"); PP_EXPECT(harness, plan.value().thumbnail_path == "data/patterns/thumbs/noise.png"); PP_EXPECT(harness, plan.value().loads_brush_resources); PP_EXPECT(harness, plan.value().update_color_ui); PP_EXPECT(harness, plan.value().update_brush_ui); } PP_EXPECT( harness, !pp::app::plan_brush_ui_texture(pp::app::BrushUiTextureSlot::tip, "", "thumb.png")); } void preset_plan_preserves_color_and_requires_brush(pp::tests::Harness& harness) { const auto plan = pp::app::plan_brush_ui_preset_replace(true); PP_EXPECT(harness, plan); if (plan) { PP_EXPECT(harness, plan.value().operation == pp::app::BrushUiOperation::replace_brush_from_preset); PP_EXPECT(harness, plan.value().preserves_existing_color); PP_EXPECT(harness, plan.value().loads_brush_resources); PP_EXPECT(harness, plan.value().update_color_ui); PP_EXPECT(harness, plan.value().update_brush_ui); } PP_EXPECT(harness, !pp::app::plan_brush_ui_preset_replace(false)); } void stroke_settings_plan_updates_brush_preview(pp::tests::Harness& harness) { const auto plan = pp::app::plan_brush_ui_stroke_settings_changed(); PP_EXPECT(harness, plan.operation == pp::app::BrushUiOperation::stroke_settings_changed); PP_EXPECT(harness, plan.mutates_brush); PP_EXPECT(harness, plan.update_color_ui); PP_EXPECT(harness, plan.update_brush_ui); PP_EXPECT(harness, !plan.loads_brush_resources); } void brush_ui_refresh_projects_requested_surfaces(pp::tests::Harness& harness) { const auto view = pp::app::plan_brush_ui_refresh(pp::app::BrushUiRefreshInput { .update_color = true, .update_brush = true, .has_current_brush = true, .has_floating_picker = true, .has_floating_color_panel = false, .tip_flow = 0.9F, .tip_size = 64.0F, .r = 0.25F, .g = 0.5F, .b = 0.75F, .a = 1.0F, }); PP_EXPECT(harness, view); if (view) { PP_EXPECT(harness, view.value().updates_stroke_controls); PP_EXPECT(harness, view.value().updates_quick_flow); PP_EXPECT(harness, view.value().updates_quick_size); PP_EXPECT(harness, view.value().updates_quick_brush_preview); PP_EXPECT(harness, view.value().updates_quick_color); PP_EXPECT(harness, view.value().updates_floating_picker); PP_EXPECT(harness, !view.value().updates_floating_color_panel); PP_EXPECT(harness, !view.value().no_op); PP_EXPECT(harness, view.value().tip_flow == 0.9F); PP_EXPECT(harness, view.value().tip_size == 64.0F); PP_EXPECT(harness, view.value().r == 0.25F); PP_EXPECT(harness, view.value().g == 0.5F); PP_EXPECT(harness, view.value().b == 0.75F); PP_EXPECT(harness, view.value().a == 1.0F); } const auto color_only = pp::app::plan_brush_ui_refresh(pp::app::BrushUiRefreshInput { .update_color = true, .update_brush = false, .has_current_brush = true, .has_floating_picker = false, .has_floating_color_panel = true, .r = 0.1F, .g = 0.2F, .b = 0.3F, .a = 1.0F, }); PP_EXPECT(harness, color_only); if (color_only) { PP_EXPECT(harness, !color_only.value().updates_stroke_controls); PP_EXPECT(harness, !color_only.value().updates_quick_flow); PP_EXPECT(harness, color_only.value().updates_quick_color); PP_EXPECT(harness, !color_only.value().updates_floating_picker); PP_EXPECT(harness, color_only.value().updates_floating_color_panel); } const auto no_op = pp::app::plan_brush_ui_refresh(pp::app::BrushUiRefreshInput { .update_color = false, .update_brush = false, .has_current_brush = false, }); PP_EXPECT(harness, no_op); if (no_op) { PP_EXPECT(harness, no_op.value().no_op); PP_EXPECT(harness, !no_op.value().updates_quick_color); PP_EXPECT(harness, !no_op.value().updates_quick_brush_preview); } } void brush_ui_refresh_rejects_invalid_state(pp::tests::Harness& harness) { PP_EXPECT(harness, !pp::app::plan_brush_ui_refresh(pp::app::BrushUiRefreshInput { .update_color = true, .update_brush = false, .has_current_brush = false, })); PP_EXPECT(harness, !pp::app::plan_brush_ui_refresh(pp::app::BrushUiRefreshInput { .update_color = true, .update_brush = false, .has_current_brush = true, .r = 1.25F, .g = 0.0F, .b = 0.0F, .a = 1.0F, })); PP_EXPECT(harness, !pp::app::plan_brush_ui_refresh(pp::app::BrushUiRefreshInput { .update_color = false, .update_brush = true, .has_current_brush = true, .tip_flow = std::nanf(""), .tip_size = 64.0F, })); } void stroke_control_plans_validate_values_and_reject_breaking_points(pp::tests::Harness& harness) { const auto slider = pp::app::plan_brush_stroke_float_setting( pp::app::BrushStrokeFloatSetting::tip_size, 42.5F); PP_EXPECT(harness, slider); if (slider) { PP_EXPECT(harness, slider.value().operation == pp::app::BrushStrokeControlOperation::set_float); PP_EXPECT(harness, slider.value().float_setting == pp::app::BrushStrokeFloatSetting::tip_size); PP_EXPECT(harness, slider.value().float_value == 42.5F); PP_EXPECT(harness, slider.value().mutates_brush); PP_EXPECT(harness, slider.value().refreshes_preview); PP_EXPECT(harness, slider.value().notifies_stroke_change); } const auto checkbox = pp::app::plan_brush_stroke_bool_setting( pp::app::BrushStrokeBoolSetting::pattern_enabled, true); PP_EXPECT(harness, checkbox.operation == pp::app::BrushStrokeControlOperation::set_bool); PP_EXPECT(harness, checkbox.bool_setting == pp::app::BrushStrokeBoolSetting::pattern_enabled); PP_EXPECT(harness, checkbox.bool_value); const auto blend = pp::app::plan_brush_stroke_blend_mode( pp::app::BrushStrokeBlendSetting::dual, 7); PP_EXPECT(harness, blend); if (blend) { PP_EXPECT(harness, blend.value().operation == pp::app::BrushStrokeControlOperation::set_blend_mode); PP_EXPECT(harness, blend.value().blend_setting == pp::app::BrushStrokeBlendSetting::dual); PP_EXPECT(harness, blend.value().blend_mode == 7); } const auto tip_aspect = pp::app::plan_brush_tip_aspect_reset(); PP_EXPECT(harness, tip_aspect.operation == pp::app::BrushStrokeControlOperation::reset_tip_aspect); PP_EXPECT(harness, tip_aspect.float_setting == pp::app::BrushStrokeFloatSetting::tip_aspect); PP_EXPECT(harness, tip_aspect.float_value == 0.5F); const auto defaults = pp::app::plan_brush_default_settings_reset(); PP_EXPECT(harness, defaults.operation == pp::app::BrushStrokeControlOperation::reset_default_brush); PP_EXPECT(harness, defaults.updates_controls); PP_EXPECT(harness, defaults.refreshes_preview); PP_EXPECT(harness, defaults.notifies_stroke_change); PP_EXPECT( harness, !pp::app::plan_brush_stroke_float_setting( pp::app::BrushStrokeFloatSetting::tip_flow, std::nanf(""))); PP_EXPECT(harness, !pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::tip, -1)); PP_EXPECT(harness, !pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::pattern, 64)); } void stroke_panel_view_projects_brush_state(pp::tests::Harness& harness) { pp::app::BrushStrokePanelInput input; input.float_values.push_back(pp::app::BrushStrokeFloatValue { .setting = pp::app::BrushStrokeFloatSetting::tip_size, .value = 42.5F, }); input.float_values.push_back(pp::app::BrushStrokeFloatValue { .setting = pp::app::BrushStrokeFloatSetting::jitter_scatter, .value = 0.75F, }); input.bool_values.push_back(pp::app::BrushStrokeBoolValue { .setting = pp::app::BrushStrokeBoolSetting::dual_enabled, .value = true, }); input.bool_values.push_back(pp::app::BrushStrokeBoolValue { .setting = pp::app::BrushStrokeBoolSetting::pattern_random_offset, .value = false, }); input.blend_values.push_back(pp::app::BrushStrokeBlendValue { .setting = pp::app::BrushStrokeBlendSetting::tip, .blend_mode = 4, }); input.blend_values.push_back(pp::app::BrushStrokeBlendValue { .setting = pp::app::BrushStrokeBlendSetting::pattern, .blend_mode = 8, }); input.tip_thumbnail_path = "data/brushes/thumbs/soft.png"; input.dual_thumbnail_path = "data/brushes/thumbs/hard.png"; input.pattern_thumbnail_path = "data/patterns/thumbs/noise.png"; const auto view = pp::app::plan_brush_stroke_panel_view(std::move(input)); PP_EXPECT(harness, view); if (view) { PP_EXPECT(harness, view.value().float_values.size() == 2); PP_EXPECT(harness, view.value().bool_values.size() == 2); PP_EXPECT(harness, view.value().blend_values.size() == 2); PP_EXPECT(harness, view.value().float_values[0].setting == pp::app::BrushStrokeFloatSetting::tip_size); PP_EXPECT(harness, view.value().float_values[0].value == 42.5F); PP_EXPECT(harness, view.value().bool_values[0].setting == pp::app::BrushStrokeBoolSetting::dual_enabled); PP_EXPECT(harness, view.value().bool_values[0].value); PP_EXPECT(harness, view.value().blend_values[1].setting == pp::app::BrushStrokeBlendSetting::pattern); PP_EXPECT(harness, view.value().blend_values[1].blend_mode == 8); PP_EXPECT(harness, view.value().tip_thumbnail_path == "data/brushes/thumbs/soft.png"); PP_EXPECT(harness, view.value().dual_thumbnail_path == "data/brushes/thumbs/hard.png"); PP_EXPECT(harness, view.value().pattern_thumbnail_path == "data/patterns/thumbs/noise.png"); PP_EXPECT(harness, view.value().updates_preview); PP_EXPECT(harness, view.value().updates_thumbnails); } } void stroke_panel_view_rejects_invalid_brush_state(pp::tests::Harness& harness) { pp::app::BrushStrokePanelInput bad_float; bad_float.float_values.push_back(pp::app::BrushStrokeFloatValue { .setting = pp::app::BrushStrokeFloatSetting::tip_flow, .value = std::nanf(""), }); PP_EXPECT(harness, !pp::app::plan_brush_stroke_panel_view(std::move(bad_float))); pp::app::BrushStrokePanelInput bad_blend; bad_blend.blend_values.push_back(pp::app::BrushStrokeBlendValue { .setting = pp::app::BrushStrokeBlendSetting::dual, .blend_mode = 64, }); PP_EXPECT(harness, !pp::app::plan_brush_stroke_panel_view(std::move(bad_blend))); } void texture_list_add_plans_target_paths_and_rejects_bad_input(pp::tests::Harness& harness) { const auto plan = pp::app::plan_brush_texture_list_add( "brushes", "D:/Paint/data", "C:/Users/artist/My Brush.JPG"); PP_EXPECT(harness, plan); if (plan) { PP_EXPECT(harness, plan.value().operation == pp::app::BrushTextureListOperation::add_texture); PP_EXPECT(harness, plan.value().source_path == "C:/Users/artist/My Brush.JPG"); PP_EXPECT(harness, plan.value().brush_name == "My Brush"); PP_EXPECT(harness, plan.value().high_path == "D:/Paint/data/brushes/My Brush.png"); PP_EXPECT(harness, plan.value().thumbnail_path == "D:/Paint/data/brushes/thumbs/My Brush.png"); PP_EXPECT(harness, plan.value().user_texture); PP_EXPECT(harness, plan.value().saves_list); PP_EXPECT(harness, plan.value().converts_brush_alpha); } const auto pattern = pp::app::plan_brush_texture_list_add( "patterns", "D:/Paint/data", R"(C:\Textures\noise.png)"); PP_EXPECT(harness, pattern); if (pattern) { PP_EXPECT(harness, !pattern.value().converts_brush_alpha); PP_EXPECT(harness, pattern.value().brush_name == "noise"); } PP_EXPECT(harness, !pp::app::plan_brush_texture_list_add("", "D:/Paint/data", "a.png")); PP_EXPECT(harness, !pp::app::plan_brush_texture_list_add("brushes", "", "a.png")); PP_EXPECT(harness, !pp::app::plan_brush_texture_list_add("brushes", "D:/Paint/data", "no-extension")); PP_EXPECT(harness, !pp::app::plan_brush_texture_list_add("brushes", "D:/Paint/data", "C:/dir/")); } void texture_list_remove_and_move_plans_handle_edges(pp::tests::Harness& harness) { const auto remove_middle = pp::app::plan_brush_texture_list_remove(3, 1, true); PP_EXPECT(harness, remove_middle); if (remove_middle) { PP_EXPECT(harness, remove_middle.value().operation == pp::app::BrushTextureListOperation::remove_texture); PP_EXPECT(harness, remove_middle.value().current_index == 1); PP_EXPECT(harness, remove_middle.value().target_index == 1); PP_EXPECT(harness, remove_middle.value().deletes_texture_files); PP_EXPECT(harness, remove_middle.value().notifies_selection); PP_EXPECT(harness, remove_middle.value().saves_list); } const auto remove_last = pp::app::plan_brush_texture_list_remove(1, 0, false); PP_EXPECT(harness, remove_last); if (remove_last) { PP_EXPECT(harness, remove_last.value().target_index == -1); PP_EXPECT(harness, !remove_last.value().deletes_texture_files); PP_EXPECT(harness, !remove_last.value().notifies_selection); } const auto move_up_edge = pp::app::plan_brush_texture_list_move(3, 0, -1); PP_EXPECT(harness, move_up_edge); if (move_up_edge) { PP_EXPECT(harness, move_up_edge.value().operation == pp::app::BrushTextureListOperation::move_texture); PP_EXPECT(harness, move_up_edge.value().target_index == 0); PP_EXPECT(harness, move_up_edge.value().no_op); } const auto move_down = pp::app::plan_brush_texture_list_move(3, 1, 1); PP_EXPECT(harness, move_down); if (move_down) { PP_EXPECT(harness, move_down.value().target_index == 2); PP_EXPECT(harness, !move_down.value().no_op); } PP_EXPECT(harness, !pp::app::plan_brush_texture_list_remove(0, 0, true)); PP_EXPECT(harness, !pp::app::plan_brush_texture_list_remove(2, 2, true)); PP_EXPECT(harness, !pp::app::plan_brush_texture_list_move(2, 0, 0)); PP_EXPECT(harness, !pp::app::plan_brush_texture_list_move(2, -1, 1)); } void preset_list_plans_add_select_move_remove_and_clear(pp::tests::Harness& harness) { const auto add = pp::app::plan_brush_preset_list_add(2, true); PP_EXPECT(harness, add); if (add) { PP_EXPECT(harness, add.value().operation == pp::app::BrushPresetListOperation::add_current_brush); PP_EXPECT(harness, add.value().target_index == 2); PP_EXPECT(harness, add.value().saves_list); PP_EXPECT(harness, add.value().updates_empty_notification); } const auto select = pp::app::plan_brush_preset_list_select(3, 1); PP_EXPECT(harness, select); if (select) { PP_EXPECT(harness, select.value().operation == pp::app::BrushPresetListOperation::select_preset); PP_EXPECT(harness, select.value().target_index == 1); PP_EXPECT(harness, select.value().selects_target); PP_EXPECT(harness, select.value().notifies_brush_changed); PP_EXPECT(harness, !select.value().saves_list); } const auto move_up_edge = pp::app::plan_brush_preset_list_move(3, 0, -1); PP_EXPECT(harness, move_up_edge); if (move_up_edge) { PP_EXPECT(harness, move_up_edge.value().target_index == 0); PP_EXPECT(harness, move_up_edge.value().no_op); PP_EXPECT(harness, move_up_edge.value().saves_list); } const auto move_down = pp::app::plan_brush_preset_list_move(3, 1, 1); PP_EXPECT(harness, move_down); if (move_down) { PP_EXPECT(harness, move_down.value().operation == pp::app::BrushPresetListOperation::move_preset); PP_EXPECT(harness, move_down.value().target_index == 2); PP_EXPECT(harness, !move_down.value().no_op); } const auto remove_middle = pp::app::plan_brush_preset_list_remove(3, 1); PP_EXPECT(harness, remove_middle); if (remove_middle) { PP_EXPECT(harness, remove_middle.value().operation == pp::app::BrushPresetListOperation::remove_preset); PP_EXPECT(harness, remove_middle.value().target_index == 1); PP_EXPECT(harness, remove_middle.value().selects_target); PP_EXPECT(harness, !remove_middle.value().clears_selection); PP_EXPECT(harness, remove_middle.value().saves_list); } const auto remove_only = pp::app::plan_brush_preset_list_remove(1, 0); PP_EXPECT(harness, remove_only); if (remove_only) { PP_EXPECT(harness, remove_only.value().target_index == -1); PP_EXPECT(harness, !remove_only.value().selects_target); PP_EXPECT(harness, remove_only.value().clears_selection); } const auto clear_empty = pp::app::plan_brush_preset_list_clear(0); PP_EXPECT(harness, clear_empty); if (clear_empty) { PP_EXPECT(harness, clear_empty.value().operation == pp::app::BrushPresetListOperation::clear_presets); PP_EXPECT(harness, clear_empty.value().no_op); PP_EXPECT(harness, clear_empty.value().saves_list); PP_EXPECT(harness, clear_empty.value().clears_selection); } } void preset_list_plans_reject_breaking_points(pp::tests::Harness& harness) { PP_EXPECT(harness, !pp::app::plan_brush_preset_list_add(-1, true)); PP_EXPECT(harness, !pp::app::plan_brush_preset_list_add(0, false)); PP_EXPECT(harness, !pp::app::plan_brush_preset_list_select(0, 0)); PP_EXPECT(harness, !pp::app::plan_brush_preset_list_select(2, 2)); PP_EXPECT(harness, !pp::app::plan_brush_preset_list_remove(0, 0)); PP_EXPECT(harness, !pp::app::plan_brush_preset_list_remove(2, -1)); PP_EXPECT(harness, !pp::app::plan_brush_preset_list_move(2, 0, 0)); PP_EXPECT(harness, !pp::app::plan_brush_preset_list_move(2, 3, 1)); PP_EXPECT(harness, !pp::app::plan_brush_preset_list_clear(-1)); } void executor_dispatches_color_and_refresh(pp::tests::Harness& harness) { FakeBrushUiServices services; const auto plan = pp::app::plan_brush_ui_color(0.25F, 0.5F, 0.75F, 1.0F); PP_EXPECT(harness, plan); if (plan) { PP_EXPECT(harness, pp::app::execute_brush_ui_plan(plan.value(), services).ok()); } PP_EXPECT(harness, services.color_sets == 1); PP_EXPECT(harness, services.last_r == 0.25F); PP_EXPECT(harness, services.last_g == 0.5F); PP_EXPECT(harness, services.last_b == 0.75F); PP_EXPECT(harness, services.last_a == 1.0F); PP_EXPECT(harness, services.refreshes == 1); PP_EXPECT(harness, services.last_update_color_ui); PP_EXPECT(harness, !services.last_update_brush_ui); PP_EXPECT(harness, services.call_order == "color;refresh;"); } void executor_dispatches_texture_and_preset(pp::tests::Harness& harness) { FakeBrushUiServices services; const auto texture = pp::app::plan_brush_ui_texture( pp::app::BrushUiTextureSlot::dual, "data/brushes/dual.png", "data/brushes/thumbs/dual.png"); PP_EXPECT(harness, texture); if (texture) { PP_EXPECT(harness, pp::app::execute_brush_ui_plan(texture.value(), services).ok()); } const auto preset = pp::app::plan_brush_ui_preset_replace(true); PP_EXPECT(harness, preset); if (preset) { PP_EXPECT(harness, pp::app::execute_brush_ui_plan(preset.value(), services).ok()); } PP_EXPECT(harness, services.texture_sets == 1); PP_EXPECT(harness, services.last_slot == pp::app::BrushUiTextureSlot::dual); PP_EXPECT(harness, services.last_path == "data/brushes/dual.png"); PP_EXPECT(harness, services.last_thumbnail_path == "data/brushes/thumbs/dual.png"); PP_EXPECT(harness, services.preset_replacements == 1); PP_EXPECT(harness, services.preserved_color); PP_EXPECT(harness, services.loaded_resources); PP_EXPECT(harness, services.refreshes == 2); PP_EXPECT(harness, services.call_order == "texture;refresh;preset;refresh;"); } void executor_dispatches_stroke_refresh_only(pp::tests::Harness& harness) { FakeBrushUiServices services; const auto plan = pp::app::plan_brush_ui_stroke_settings_changed(); PP_EXPECT(harness, pp::app::execute_brush_ui_plan(plan, services).ok()); PP_EXPECT(harness, services.color_sets == 0); PP_EXPECT(harness, services.texture_sets == 0); PP_EXPECT(harness, services.preset_replacements == 0); PP_EXPECT(harness, services.refreshes == 1); PP_EXPECT(harness, services.last_update_color_ui); PP_EXPECT(harness, services.last_update_brush_ui); PP_EXPECT(harness, services.call_order == "refresh;"); } void texture_list_executor_dispatches_and_preserves_failure(pp::tests::Harness& harness) { FakeBrushTextureListServices services; const auto add = pp::app::plan_brush_texture_list_add("brushes", "D:/Paint/data", "C:/Temp/soft.png"); PP_EXPECT(harness, add); if (add) { PP_EXPECT(harness, pp::app::execute_brush_texture_list_plan(add.value(), services).ok()); } const auto remove = pp::app::plan_brush_texture_list_remove(3, 2, true); PP_EXPECT(harness, remove); if (remove) { PP_EXPECT(harness, pp::app::execute_brush_texture_list_plan(remove.value(), services).ok()); } const auto move = pp::app::plan_brush_texture_list_move(3, 1, -1); PP_EXPECT(harness, move); if (move) { PP_EXPECT(harness, pp::app::execute_brush_texture_list_plan(move.value(), services).ok()); } PP_EXPECT(harness, services.adds == 1); PP_EXPECT(harness, services.last_source_path == "C:/Temp/soft.png"); PP_EXPECT(harness, services.last_high_path == "D:/Paint/data/brushes/soft.png"); PP_EXPECT(harness, services.last_thumbnail_path == "D:/Paint/data/brushes/thumbs/soft.png"); PP_EXPECT(harness, services.last_brush_name == "soft"); PP_EXPECT(harness, services.last_converts_brush_alpha); PP_EXPECT(harness, services.removes == 1); PP_EXPECT(harness, services.moves == 1); PP_EXPECT(harness, services.selections == 1); PP_EXPECT(harness, services.saves == 3); PP_EXPECT(harness, services.last_deletes_texture_files); PP_EXPECT(harness, services.call_order == "add;save;remove;select;save;move;save;"); FakeBrushTextureListServices failing_services; failing_services.fail_add = true; PP_EXPECT(harness, add); if (add) { PP_EXPECT(harness, !pp::app::execute_brush_texture_list_plan(add.value(), failing_services).ok()); } PP_EXPECT(harness, failing_services.saves == 0); PP_EXPECT(harness, failing_services.call_order == "add-failed;"); } void preset_list_executor_dispatches_and_preserves_failure(pp::tests::Harness& harness) { FakeBrushPresetListServices services; const auto add = pp::app::plan_brush_preset_list_add(2, true); PP_EXPECT(harness, add); if (add) { PP_EXPECT(harness, pp::app::execute_brush_preset_list_plan(add.value(), services).ok()); } const auto select = pp::app::plan_brush_preset_list_select(3, 1); PP_EXPECT(harness, select); if (select) { PP_EXPECT(harness, pp::app::execute_brush_preset_list_plan(select.value(), services).ok()); } const auto move = pp::app::plan_brush_preset_list_move(3, 2, -1); PP_EXPECT(harness, move); if (move) { PP_EXPECT(harness, pp::app::execute_brush_preset_list_plan(move.value(), services).ok()); } const auto remove = pp::app::plan_brush_preset_list_remove(3, 1); PP_EXPECT(harness, remove); if (remove) { PP_EXPECT(harness, pp::app::execute_brush_preset_list_plan(remove.value(), services).ok()); } const auto clear = pp::app::plan_brush_preset_list_clear(0); PP_EXPECT(harness, clear); if (clear) { PP_EXPECT(harness, pp::app::execute_brush_preset_list_plan(clear.value(), services).ok()); } PP_EXPECT(harness, services.adds == 1); PP_EXPECT(harness, services.selections == 1); PP_EXPECT(harness, services.moves == 1); PP_EXPECT(harness, services.removes == 1); PP_EXPECT(harness, services.clears == 1); PP_EXPECT(harness, services.notification_updates == 3); PP_EXPECT(harness, services.saves == 4); PP_EXPECT(harness, services.last_index == 1); PP_EXPECT(harness, services.last_target_index == 1); PP_EXPECT(harness, services.last_selects_target); PP_EXPECT(harness, !services.last_remove_clears_selection); PP_EXPECT(harness, services.last_clear_clears_selection); PP_EXPECT(harness, services.last_notifies_brush_changed); PP_EXPECT( harness, services.call_order == "add;notification;save;select;move;save;remove;notification;save;clear;" "notification;save;"); FakeBrushPresetListServices failing_services; failing_services.fail_add = true; PP_EXPECT(harness, add); if (add) { PP_EXPECT(harness, !pp::app::execute_brush_preset_list_plan(add.value(), failing_services).ok()); } PP_EXPECT(harness, failing_services.saves == 0); PP_EXPECT(harness, failing_services.call_order == "add-failed;"); } void stroke_control_executor_dispatches_and_rejects_bad_payloads(pp::tests::Harness& harness) { FakeBrushStrokeControlServices services; const auto slider = pp::app::plan_brush_stroke_float_setting( pp::app::BrushStrokeFloatSetting::pattern_depth, 0.75F); PP_EXPECT(harness, slider); if (slider) { PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(slider.value(), services).ok()); } const auto checkbox = pp::app::plan_brush_stroke_bool_setting( pp::app::BrushStrokeBoolSetting::dual_enabled, true); PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(checkbox, services).ok()); const auto blend = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::pattern, 3); PP_EXPECT(harness, blend); if (blend) { PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(blend.value(), services).ok()); } PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(pp::app::plan_brush_tip_aspect_reset(), services).ok()); PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(pp::app::plan_brush_default_settings_reset(), services).ok()); PP_EXPECT(harness, services.float_sets == 1); PP_EXPECT(harness, services.last_float_setting == pp::app::BrushStrokeFloatSetting::pattern_depth); PP_EXPECT(harness, services.last_bool_setting == pp::app::BrushStrokeBoolSetting::dual_enabled); PP_EXPECT(harness, services.last_blend_setting == pp::app::BrushStrokeBlendSetting::pattern); PP_EXPECT(harness, services.last_blend_mode == 3); PP_EXPECT(harness, services.tip_aspect_resets == 1); PP_EXPECT(harness, services.default_resets == 1); PP_EXPECT(harness, services.preview_refreshes == 5); PP_EXPECT(harness, services.control_updates == 1); PP_EXPECT(harness, services.stroke_notifications == 5); PP_EXPECT( harness, services.call_order == "float;preview;notify;bool;preview;notify;blend;preview;notify;tip-aspect;" "preview;notify;default;controls;preview;notify;"); FakeBrushStrokeControlServices bad_services; pp::app::BrushStrokeControlPlan bad_float; bad_float.operation = pp::app::BrushStrokeControlOperation::set_float; bad_float.float_value = std::nanf(""); PP_EXPECT(harness, !pp::app::execute_brush_stroke_control_plan(bad_float, bad_services).ok()); pp::app::BrushStrokeControlPlan bad_blend; bad_blend.operation = pp::app::BrushStrokeControlOperation::set_blend_mode; bad_blend.blend_mode = 99; PP_EXPECT(harness, !pp::app::execute_brush_stroke_control_plan(bad_blend, bad_services).ok()); PP_EXPECT(harness, bad_services.call_order.empty()); } void executor_rejects_invalid_plan_payloads(pp::tests::Harness& harness) { FakeBrushUiServices services; pp::app::BrushUiPlan color; color.operation = pp::app::BrushUiOperation::set_tip_color; color.r = std::nanf(""); color.g = 0.0F; color.b = 0.0F; color.a = 1.0F; PP_EXPECT(harness, !pp::app::execute_brush_ui_plan(color, services).ok()); pp::app::BrushUiPlan texture; texture.operation = pp::app::BrushUiOperation::set_texture; texture.texture_slot = pp::app::BrushUiTextureSlot::tip; texture.path.clear(); PP_EXPECT(harness, !pp::app::execute_brush_ui_plan(texture, services).ok()); PP_EXPECT(harness, services.color_sets == 0); PP_EXPECT(harness, services.texture_sets == 0); PP_EXPECT(harness, services.refreshes == 0); FakeBrushTextureListServices list_services; pp::app::BrushTextureListPlan add; add.operation = pp::app::BrushTextureListOperation::add_texture; add.source_path = "source.png"; PP_EXPECT(harness, !pp::app::execute_brush_texture_list_plan(add, list_services).ok()); pp::app::BrushTextureListPlan move; move.operation = pp::app::BrushTextureListOperation::move_texture; move.item_count = 1; move.current_index = 0; move.target_index = 1; PP_EXPECT(harness, !pp::app::execute_brush_texture_list_plan(move, list_services).ok()); PP_EXPECT(harness, list_services.call_order.empty()); FakeBrushPresetListServices preset_services; pp::app::BrushPresetListPlan remove_preset; remove_preset.operation = pp::app::BrushPresetListOperation::remove_preset; remove_preset.item_count = 1; remove_preset.current_index = 0; remove_preset.target_index = 1; remove_preset.selects_target = true; PP_EXPECT(harness, !pp::app::execute_brush_preset_list_plan(remove_preset, preset_services).ok()); pp::app::BrushPresetListPlan select_preset; select_preset.operation = pp::app::BrushPresetListOperation::select_preset; select_preset.item_count = 1; select_preset.target_index = 1; PP_EXPECT(harness, !pp::app::execute_brush_preset_list_plan(select_preset, preset_services).ok()); PP_EXPECT(harness, preset_services.call_order.empty()); } } // namespace int main() { pp::tests::Harness harness; harness.run("color plan validates all channels", color_plan_validates_all_channels); harness.run("texture plan validates path and slot", texture_plan_validates_path_and_slot); harness.run("preset plan preserves color and requires brush", preset_plan_preserves_color_and_requires_brush); harness.run("stroke settings plan updates brush preview", stroke_settings_plan_updates_brush_preview); harness.run("brush UI refresh projects requested surfaces", brush_ui_refresh_projects_requested_surfaces); harness.run("brush UI refresh rejects invalid state", brush_ui_refresh_rejects_invalid_state); harness.run("stroke control plans validate values and reject breaking points", stroke_control_plans_validate_values_and_reject_breaking_points); harness.run("stroke panel view projects brush state", stroke_panel_view_projects_brush_state); harness.run("stroke panel view rejects invalid brush state", stroke_panel_view_rejects_invalid_brush_state); harness.run("texture list add plans target paths and rejects bad input", texture_list_add_plans_target_paths_and_rejects_bad_input); harness.run("texture list remove and move plans handle edges", texture_list_remove_and_move_plans_handle_edges); harness.run("preset list plans add select move remove and clear", preset_list_plans_add_select_move_remove_and_clear); harness.run("preset list plans reject breaking points", preset_list_plans_reject_breaking_points); harness.run("executor dispatches color and refresh", executor_dispatches_color_and_refresh); harness.run("executor dispatches texture and preset", executor_dispatches_texture_and_preset); harness.run("executor dispatches stroke refresh only", executor_dispatches_stroke_refresh_only); harness.run("texture list executor dispatches and preserves failure", texture_list_executor_dispatches_and_preserves_failure); harness.run("preset list executor dispatches and preserves failure", preset_list_executor_dispatches_and_preserves_failure); harness.run("stroke control executor dispatches and rejects bad payloads", stroke_control_executor_dispatches_and_rejects_bad_payloads); harness.run("executor rejects invalid plan payloads", executor_rejects_invalid_plan_payloads); return harness.finish(); }