Modules/Sketch2D/tests/BrushToolsTests.cpp
Шурупов Илья Викторович ebc22c5f38 Use one menu for the menu bar and the tablet menu pages
The actions and settings split is reverted. Sketch2DRoot::mainMenu is
the single menu structure: on PC it fills the menu bar and the studio
gear is hidden, on the tablet the gear opens it as pages.
2026-10-20 02:31:43 +03:00

390 lines
14 KiB
C++

#include "FakeWindowHost.hpp"
#include "Sketch2DUI.hpp"
#include "StudioUI.hpp"
#include "Ui.hpp"
#include "UiDriver.hpp"
#include "UnitTest++/UnitTest++.h"
#include <cmath>
using namespace tp;
using namespace tp::sketch2d;
using namespace tp::ui;
using namespace tp::ui::testing;
namespace {
class ToolsViewport : public ViewportRenderer {
public:
void render(Document&, const ViewSettings&, Vec2F) override {}
[[nodiscard]] ImageId image() const override { return { 1 }; }
void setDebugOverlay(bool) override {}
[[nodiscard]] bool debugOverlay() const override { return false; }
};
class ToolsIcons : public IconProvider {
public:
[[nodiscard]] ImageId icon(const std::string& name) const override { return { (ualni) name.size() + 1 }; }
};
struct ToolsScene {
Document document;
Brush brush;
ToolsViewport viewport;
ToolsIcons icons;
Sketch2DRoot root{ document, viewport, brush, icons };
ToolsScene() {
root.canvas.settings().viewStiffness = 1;
brush.apply(*findBrushPreset(BrushType::Round));
}
};
struct TabletScene {
Document document;
Brush brush;
ToolsViewport viewport;
ToolsIcons icons;
Sketch2DRoot classic{ document, viewport, brush, icons };
StudioRoot studio{ classic, brush, icons };
TabletScene() {
classic.canvas.settings().viewStiffness = 1;
brush.apply(*findBrushPreset(BrushType::Round));
}
};
const MenuItem* findItem(Menu& menu, const std::string& text) {
for (const auto& item : menu.items())
if (item->text() == text) return item.get();
return nullptr;
}
void choose(Menu& menu, const std::string& text) {
const MenuItem* item = findItem(menu, text);
CHECK(item != nullptr);
if (item) item->entry().action();
}
size_t strokesOf(Document& document) { return document.history().revision(); }
}
SUITE(BrushReset) {
TEST(ResetEverythingRestoresThePresetAndKeepsColor) {
Brush brush;
brush.apply(*findBrushPreset(BrushType::Ink));
const Brush original = brush;
brush.color = { 0.1f, 0.2f, 0.3f, 1 };
brush.setSize(321);
brush.set(BrushParameter::Texture, 0.9f);
brush.setStabilizer(0.9f);
brush.resetEverything();
CHECK_CLOSE(original.size(), brush.size(), 1e-4f);
CHECK_CLOSE(original.get(BrushParameter::Texture), brush.get(BrushParameter::Texture), 1e-4f);
CHECK_CLOSE(Brush::kDefaultStabilizer, brush.stabilizer(), 1e-4f);
CHECK_CLOSE(0.1f, brush.color.r, 1e-6f);
CHECK_EQUAL(std::string(original.presetName()), brush.presetName());
}
TEST(ResetParametersKeepsTexturesAndStabilization) {
Brush brush;
brush.apply(*findBrushPreset(BrushType::Pencil));
const Brush original = brush;
brush.setSize(555);
brush.setOpacity(0.2f);
brush.set(BrushParameter::Texture, 0.77f);
brush.set(BrushParameter::GrainScale, 4.5f);
brush.setStabilizer(0.8f);
brush.resetParameters();
CHECK_CLOSE(original.size(), brush.size(), 1e-4f);
CHECK_CLOSE(original.opacity(), brush.opacity(), 1e-4f);
CHECK_CLOSE(0.77f, brush.get(BrushParameter::Texture), 1e-4f);
CHECK_CLOSE(4.5f, brush.get(BrushParameter::GrainScale), 1e-4f);
CHECK_CLOSE(0.8f, brush.stabilizer(), 1e-4f);
}
TEST(ResetTexturesOnlyTouchesTextureValues) {
Brush brush;
brush.apply(*findBrushPreset(BrushType::Pencil));
const Brush original = brush;
brush.setSize(555);
brush.set(BrushParameter::Texture, 0.11f);
brush.set(BrushParameter::DeckleScale, 9);
brush.resetTextures();
CHECK_CLOSE(555.0f, brush.size(), 1e-4f);
CHECK_CLOSE(original.get(BrushParameter::Texture), brush.get(BrushParameter::Texture), 1e-4f);
CHECK_CLOSE(original.get(BrushParameter::DeckleScale), brush.get(BrushParameter::DeckleScale), 1e-4f);
}
TEST(ResetStabilizationOnlyTouchesTheStabilizer) {
Brush brush;
brush.apply(*findBrushPreset(BrushType::Round));
brush.setSize(77);
brush.setStabilizer(0.9f);
brush.resetStabilization();
CHECK_CLOSE(Brush::kDefaultStabilizer, brush.stabilizer(), 1e-4f);
CHECK_CLOSE(77.0f, brush.size(), 1e-4f);
}
TEST(SizeStepsAlwaysMoveAndStayWithinLimits) {
Brush brush;
brush.setSize(1);
brush.scaleSize(1.2f);
CHECK_CLOSE(2.0f, brush.size(), 1e-4f);
brush.setSize(100);
brush.scaleSize(1.2f);
CHECK_CLOSE(120.0f, brush.size(), 1e-4f);
brush.scaleSize(1.0f / 1.2f);
CHECK_CLOSE(100.0f, brush.size(), 1e-4f);
brush.setSize(1);
brush.scaleSize(1.0f / 1.2f);
CHECK_CLOSE(1.0f, brush.size(), 1e-4f);
brush.setSize(Brush::kMaximumSize);
brush.scaleSize(1.2f);
CHECK_CLOSE(Brush::kMaximumSize, brush.size(), 1e-4f);
}
TEST(AngleWrapsAroundAFullTurn) {
Brush brush;
brush.set(BrushParameter::Angle, 350);
brush.adjust(BrushParameter::Angle, 15);
CHECK_CLOSE(5.0f, brush.get(BrushParameter::Angle), 1e-3f);
brush.adjust(BrushParameter::Angle, -15);
CHECK_CLOSE(350.0f, brush.get(BrushParameter::Angle), 1e-3f);
}
}
SUITE(BrushCommands) {
TEST(CommandIdsMatchTheReference) {
CHECK_EQUAL(1000178, (int4) Command::ToolBrushAngle);
CHECK_EQUAL(1000179, (int4) Command::ToolBrushSize);
CHECK_EQUAL(1000183, (int4) Command::HoldBrushFlow);
CHECK_EQUAL(1000185, (int4) Command::HoldBrushOpacity);
CHECK_EQUAL(1000202, (int4) Command::ResetBrush);
CHECK_EQUAL(1000206, (int4) Command::BrushAngleDecrease);
CHECK_EQUAL(1000209, (int4) Command::BrushFlowIncrease);
CHECK_EQUAL(1000211, (int4) Command::BrushOpacityIncrease);
CHECK_EQUAL(std::string("Reset brush"), commandInfo(Command::ResetBrush).label);
CHECK_EQUAL(std::string("Brush flow tool"), commandInfo(Command::HoldBrushFlow).label);
CHECK_EQUAL(std::string("Opacity increase"), commandInfo(Command::BrushOpacityIncrease).label);
}
TEST(BracketKeysResizeTheBrush) {
ToolsScene scene;
UiDriver driver(scene.root, { 1280, 800 });
scene.brush.setSize(100);
driver.key(InputID::RIGHT_BRACKET);
CHECK_CLOSE(115.0f, scene.brush.size(), 1e-3f);
driver.holdKey(InputID::LEFT_BRACKET, 2);
CHECK_CLOSE(71.0f, scene.brush.size(), 1e-3f);
CHECK_EQUAL(std::string("71 px"), scene.root.lastNotice());
CHECK_EQUAL(std::string("71 px"), scene.root.toolBar.brushSize.text());
}
TEST(DigitKeysSetBrushOpacity) {
ToolsScene scene;
UiDriver driver(scene.root, { 1280, 800 });
driver.key(InputID::N3);
CHECK_CLOSE(0.3f, scene.brush.opacity(), 1e-4f);
driver.key(InputID::N0);
CHECK_CLOSE(1.0f, scene.brush.opacity(), 1e-4f);
driver.key(InputID::N1);
CHECK_CLOSE(0.1f, scene.brush.opacity(), 1e-4f);
}
TEST(StepCommandsAdjustFlowOpacityAndAngle) {
ToolsScene scene;
UiDriver driver(scene.root, { 1280, 800 });
scene.brush.setFlow(0.5f);
scene.brush.setOpacity(0.5f);
scene.root.commands.run(Command::BrushFlowIncrease);
scene.root.commands.run(Command::BrushOpacityDecrease);
scene.root.commands.run(Command::BrushAngleIncrease);
CHECK_CLOSE(0.55f, scene.brush.flow(), 1e-4f);
CHECK_CLOSE(0.45f, scene.brush.opacity(), 1e-4f);
CHECK_CLOSE(kBrushAngleStep, scene.brush.get(BrushParameter::Angle), 1e-4f);
}
TEST(BrushMenuListsTheBrushCommands) {
ToolsScene scene;
UiDriver driver(scene.root, { 1280, 800 });
Menu& menu = scene.root.brushMenu;
for (const char* text : { "Reset all brushes",
"Smaller brush",
"Bigger brush",
"Reset brush",
"Show Brush list",
"Show Brush options",
"Adjust",
"Opacity",
"Stabilizer" })
CHECK(findItem(menu, text) != nullptr);
for (const char* text : { "Reset all brushes (everything)",
"Reset all brush parameters",
"Reset all brush stabilization",
"Reset all brush textures" })
CHECK(findItem(scene.root.resetMenu, text) != nullptr);
CHECK_EQUAL((size_t) 10, scene.root.brushOpacityMenu.items().size());
CHECK_EQUAL(std::string("]"), findItem(menu, "Bigger brush")->entry().shortcut);
}
TEST(ResetMenuResetsTheCurrentBrush) {
ToolsScene scene;
UiDriver driver(scene.root, { 1280, 800 });
const halnf original = scene.brush.size();
scene.brush.setSize(999);
scene.brush.setStabilizer(0.95f);
choose(scene.root.resetMenu, "Reset all brush parameters");
CHECK_CLOSE(original, scene.brush.size(), 1e-4f);
CHECK_CLOSE(0.95f, scene.brush.stabilizer(), 1e-4f);
choose(scene.root.resetMenu, "Reset all brush stabilization");
CHECK_CLOSE(Brush::kDefaultStabilizer, scene.brush.stabilizer(), 1e-4f);
}
TEST(ResetAllBrushesRestoresThePaintingBrush) {
ToolsScene scene;
UiDriver driver(scene.root, { 1280, 800 });
scene.brush.apply(*findBrushPreset(BrushType::Ink));
scene.brush.setSize(800);
driver.click(scene.root.toolStrip.button("eraser"));
scene.root.commands.run(Command::ResetAllBrushes);
driver.click(scene.root.toolStrip.button("brush"));
CHECK(scene.brush.type == BrushType::Round);
CHECK(scene.brush.size() < 800);
}
TEST(OptionsPanelResetButtonResetsTheBrush) {
ToolsScene scene;
UiDriver driver(scene.root, { 1280, 800 });
const halnf original = scene.brush.size();
scene.brush.setSize(444);
scene.root.refreshFromState();
driver.layout();
driver.click(scene.root.toolBar.brushSize);
REQUIRE CHECK(driver.ui().isPopupOpen(scene.root.toolBar.brushPopup));
driver.click(scene.root.toolBar.brushPopup.reset);
CHECK_CLOSE(original, scene.brush.size(), 1e-4f);
CHECK_CLOSE(original, scene.root.toolBar.brushPopup.size.value(), 0.5f);
}
TEST(PanelShortcutsOpenTheBrushPopups) {
ToolsScene scene;
UiDriver driver(scene.root, { 1280, 800 });
driver.moveTo(driver.centerOf(scene.root.canvas));
driver.key(InputID::F4);
CHECK(driver.ui().isPopupOpen(scene.root.toolBar.brushListPopup));
driver.key(InputID::ESCAPE);
driver.key(InputID::F5);
CHECK(driver.ui().isPopupOpen(scene.root.toolBar.brushPopup));
}
}
SUITE(BrushDragTools) {
TEST(DragValuesFollowTheReferenceFeel) {
CHECK_CLOSE(
200.0f, CanvasWidget::adjustedValue(BrushParameter::Size, 100, CanvasWidget::kSizeDoublingPixels), 0.5f
);
CHECK_CLOSE(
50.0f, CanvasWidget::adjustedValue(BrushParameter::Size, 100, -CanvasWidget::kSizeDoublingPixels), 0.5f
);
CHECK_CLOSE(10.0f, CanvasWidget::adjustedValue(BrushParameter::Angle, 350, 20), 1e-3f);
CHECK_CLOSE(0.75f, CanvasWidget::adjustedValue(BrushParameter::Opacity, 0.5f, 50), 1e-4f);
}
TEST(HoldingDAndDraggingResizesWithoutPainting) {
ToolsScene scene;
UiDriver driver(scene.root, { 1280, 800 });
scene.brush.setSize(50);
const Vec2F center = driver.centerOf(scene.root.canvas);
driver.moveTo(center);
const size_t before = strokesOf(scene.document);
driver.pressKey(InputID::D);
driver.press();
CHECK(scene.root.canvas.isAdjustingBrush());
driver.moveTo(center + Vec2F{ CanvasWidget::kSizeDoublingPixels, 0.0f });
CHECK_CLOSE(100.0f, scene.brush.size(), 0.5f);
CHECK_EQUAL(std::string("100 px"), scene.root.toolBar.brushSize.text());
CHECK(scene.root.canvas.brushCursor().contour);
bool labelled = false;
for (const PaintCommand& command : driver.paint().commands())
if (command.kind == PaintCommand::Kind::Text && command.text == "Size 100 px") labelled = true;
CHECK(labelled);
driver.release();
driver.releaseKey(InputID::D);
CHECK(!scene.root.canvas.isAdjustingBrush());
CHECK_EQUAL(before, strokesOf(scene.document));
driver.drag(center, center + Vec2F{ 40, 0 });
CHECK_CLOSE(100.0f, scene.brush.size(), 0.5f);
CHECK(strokesOf(scene.document) > before);
}
TEST(HoldingADragsTheAngle) {
ToolsScene scene;
UiDriver driver(scene.root, { 1280, 800 });
const Vec2F center = driver.centerOf(scene.root.canvas);
driver.moveTo(center);
driver.pressKey(InputID::A);
driver.press();
driver.moveTo(center + Vec2F{ 30, 0 });
driver.release();
driver.releaseKey(InputID::A);
CHECK_CLOSE(30.0f, scene.brush.get(BrushParameter::Angle), 0.01f);
}
TEST(FlowAndOpacityHoldsDragTheirAmounts) {
ToolsScene scene;
UiDriver driver(scene.root, { 1280, 800 });
scene.root.commands.shortcuts().bind(plain(InputID::F), Command::HoldBrushFlow);
scene.root.commands.shortcuts().bind(plain(InputID::O), Command::HoldBrushOpacity);
scene.brush.setFlow(0.5f);
scene.brush.setOpacity(0.5f);
const Vec2F center = driver.centerOf(scene.root.canvas);
driver.moveTo(center);
driver.pressKey(InputID::F);
driver.press();
driver.moveTo(center + Vec2F{ -40, 0 });
driver.release();
driver.releaseKey(InputID::F);
CHECK_CLOSE(0.3f, scene.brush.flow(), 1e-3f);
CHECK_CLOSE(0.5f, scene.brush.opacity(), 1e-4f);
scene.brush.setOpacity(0.2f);
driver.moveTo(center);
driver.pressKey(InputID::O);
driver.press();
driver.moveTo(center + Vec2F{ 60, 0 });
driver.release();
driver.releaseKey(InputID::O);
CHECK_CLOSE(0.5f, scene.brush.opacity(), 1e-3f);
}
TEST(BrushSizeToolStaysOnUntilToggledOff) {
ToolsScene scene;
UiDriver driver(scene.root, { 1280, 800 });
scene.brush.setSize(40);
scene.root.commands.run(Command::ToolBrushSize);
CHECK(scene.root.commands.isChecked(Command::ToolBrushSize));
const Vec2F center = driver.centerOf(scene.root.canvas);
driver.drag(center, center + Vec2F{ CanvasWidget::kSizeDoublingPixels, 0.0f });
CHECK_CLOSE(80.0f, scene.brush.size(), 0.5f);
driver.drag(center, center + Vec2F{ CanvasWidget::kSizeDoublingPixels, 0.0f });
CHECK_CLOSE(160.0f, scene.brush.size(), 0.5f);
scene.root.commands.run(Command::ToolBrushSize);
CHECK(!scene.root.commands.isChecked(Command::ToolBrushSize));
CHECK(!scene.root.canvas.adjustTool().has_value());
}
}
SUITE(TabletBrushTools) {
TEST(TabletSettingsReachTheBrushMenu) {
TabletScene scene;
UiDriver driver(scene.studio, { 1280, 800 });
driver.layout();
bool found = false;
for (const auto& entry : scene.classic.mainMenu.items())
if (entry->entry().text == "Brush" && entry->entry().submenu == &scene.classic.brushMenu) found = true;
CHECK(found);
CHECK(findItem(scene.classic.brushMenu, "Reset brush") != nullptr);
}
}