Merge pull request #27428 from kadet1090/add-style-parameter-tuples
Gui: Add Tuple type to StyleParameters
This commit is contained in:
@@ -1402,6 +1402,7 @@ SET(FreeCADGui_CPP_SRCS
|
||||
StartupProcess.cpp
|
||||
TransactionObject.cpp
|
||||
ToolHandler.cpp
|
||||
StyleParameters/Value.cpp
|
||||
StyleParameters/Parser.cpp
|
||||
StyleParameters/ParameterManager.cpp
|
||||
)
|
||||
@@ -1447,6 +1448,7 @@ SET(FreeCADGui_SRCS
|
||||
StatusBarLabel.h
|
||||
TransactionObject.h
|
||||
ToolHandler.h
|
||||
StyleParameters/Value.h
|
||||
StyleParameters/Parser.h
|
||||
StyleParameters/ParameterManager.h
|
||||
)
|
||||
|
||||
@@ -66,6 +66,9 @@ QString typeOfTokenValue(const Gui::StyleParameters::Value& value)
|
||||
},
|
||||
[](const Base::Color&) {
|
||||
return QWidget::tr("Color");
|
||||
},
|
||||
[](const Gui::StyleParameters::Tuple&) {
|
||||
return QWidget::tr("Tuple");
|
||||
}
|
||||
},
|
||||
value
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
#include <fstream>
|
||||
#include <yaml-cpp/yaml.h>
|
||||
|
||||
#include <QColor>
|
||||
#include <QRegularExpression>
|
||||
#include <QString>
|
||||
#include <ranges>
|
||||
@@ -42,72 +41,6 @@ FC_LOG_LEVEL_INIT("Gui", true, true)
|
||||
namespace Gui::StyleParameters
|
||||
{
|
||||
|
||||
Numeric Numeric::operator+(const Numeric& rhs) const
|
||||
{
|
||||
ensureEqualUnits(rhs);
|
||||
return {value + rhs.value, unit};
|
||||
}
|
||||
|
||||
Numeric Numeric::operator-(const Numeric& rhs) const
|
||||
{
|
||||
ensureEqualUnits(rhs);
|
||||
return {value - rhs.value, unit};
|
||||
}
|
||||
|
||||
Numeric Numeric::operator-() const
|
||||
{
|
||||
return {-value, unit};
|
||||
}
|
||||
|
||||
Numeric Numeric::operator/(const Numeric& rhs) const
|
||||
{
|
||||
if (rhs.value == 0) {
|
||||
THROWM(Base::RuntimeError, "Division by zero");
|
||||
}
|
||||
|
||||
if (rhs.unit.empty() || unit.empty()) {
|
||||
return {value / rhs.value, unit};
|
||||
}
|
||||
|
||||
ensureEqualUnits(rhs);
|
||||
return {value / rhs.value, unit};
|
||||
}
|
||||
|
||||
Numeric Numeric::operator*(const Numeric& rhs) const
|
||||
{
|
||||
if (rhs.unit.empty() || unit.empty()) {
|
||||
return {value * rhs.value, unit};
|
||||
}
|
||||
|
||||
ensureEqualUnits(rhs);
|
||||
return {value * rhs.value, unit};
|
||||
}
|
||||
|
||||
void Numeric::ensureEqualUnits(const Numeric& rhs) const
|
||||
{
|
||||
if (unit != rhs.unit) {
|
||||
THROWM(
|
||||
Base::RuntimeError,
|
||||
fmt::format("Units mismatch left expression is '{}', right expression is '{}'", unit, rhs.unit)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
std::string Value::toString() const
|
||||
{
|
||||
if (std::holds_alternative<Numeric>(*this)) {
|
||||
auto [value, unit] = std::get<Numeric>(*this);
|
||||
return fmt::format("{}{}", value, unit);
|
||||
}
|
||||
|
||||
if (std::holds_alternative<Base::Color>(*this)) {
|
||||
auto color = std::get<Base::Color>(*this);
|
||||
return fmt::format("#{:0>6x}", color.getPackedRGB() >> 8); // NOLINT(*-magic-numbers)
|
||||
}
|
||||
|
||||
return std::get<std::string>(*this);
|
||||
}
|
||||
|
||||
ParameterSource::ParameterSource(const Metadata& metadata)
|
||||
: metadata(metadata)
|
||||
{}
|
||||
|
||||
@@ -28,14 +28,13 @@
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QColor>
|
||||
|
||||
#include <App/Application.h>
|
||||
#include <Base/Bitmask.h>
|
||||
#include <Base/Parameter.h>
|
||||
|
||||
#include "Value.h"
|
||||
|
||||
// That macro uses inline const because some older compilers to not properly support constexpr
|
||||
// for std::string. It should be changed into static constepxr once we migrate to newer compiler.
|
||||
#define DEFINE_STYLE_PARAMETER(_name_, _defaultValue_) \
|
||||
@@ -50,82 +49,6 @@ namespace Gui::StyleParameters
|
||||
// Forward declaration for Parser
|
||||
class Parser;
|
||||
|
||||
/**
|
||||
* @brief Represents a length in a specified unit.
|
||||
*
|
||||
* This struct is a very simplified representation of lengths that can be used as parameters for
|
||||
* styling purposes. The length basically consists of value and unit. Unit is optional, empty unit
|
||||
* represents a dimensionless length that can be used as a scalar. This struct does not care about
|
||||
* unit conversions as its uses do not require it.
|
||||
*/
|
||||
struct Numeric
|
||||
{
|
||||
/// Numeric value of the length.
|
||||
double value;
|
||||
/// Unit of the length, empty if the value is dimensionless.
|
||||
std::string unit = "";
|
||||
|
||||
/**
|
||||
* @name Operators
|
||||
*
|
||||
* This struct supports basic operations on Length. Each operation requires for operands to be
|
||||
* the same unit. Multiplication and division additionally allow one operand to be dimensionless
|
||||
* and hence act as a scalar.
|
||||
*
|
||||
* @code{c++}
|
||||
* Numeric a { 10, "px" };
|
||||
* Numeric b { 5, "px" };
|
||||
*
|
||||
* Numeric differentUnit { 3, "rem" }
|
||||
* Numeric scalar { 2, "" };
|
||||
*
|
||||
* // basic operations with the same unit are allowed
|
||||
* auto sum = a + b; // 15 px
|
||||
* auto difference = a - 5; // 10 px
|
||||
*
|
||||
* // basic operations with mixed units are NOT allowed
|
||||
* auto sumOfIncompatibleUnits = a + differentUnit; // will throw
|
||||
* auto productOfIncompatibleUnits = a * differentUnit; // will throw
|
||||
*
|
||||
* // exception is that for multiplication and division dimensionless units are allowed
|
||||
* auto productWithScalar = a * scalar; // 20 px
|
||||
* @endcode
|
||||
* @{
|
||||
*/
|
||||
Numeric operator+(const Numeric& rhs) const;
|
||||
Numeric operator-(const Numeric& rhs) const;
|
||||
Numeric operator-() const;
|
||||
|
||||
Numeric operator/(const Numeric& rhs) const;
|
||||
Numeric operator*(const Numeric& rhs) const;
|
||||
/// @}
|
||||
|
||||
private:
|
||||
void ensureEqualUnits(const Numeric& rhs) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief This struct represents any valid value that can be used as the parameter value.
|
||||
*
|
||||
* The value can be one of three basic types:
|
||||
* - Numbers / Lengths (so any length with optional unit) (Length)
|
||||
* - Colors (QColor)
|
||||
* - Any other generic expression. (std::string)
|
||||
*
|
||||
* As a rule, operations can be only performed over values of the same type.
|
||||
*/
|
||||
struct Value: std::variant<Numeric, Base::Color, std::string>
|
||||
{
|
||||
using std::variant<Numeric, Base::Color, std::string>::variant;
|
||||
|
||||
/**
|
||||
* Converts the object into its string representation.
|
||||
*
|
||||
* @return A string representation of the object that can later be used in QSS.
|
||||
*/
|
||||
std::string toString() const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A structure to define parameters which can be referenced in the code.
|
||||
*
|
||||
|
||||
@@ -28,9 +28,7 @@
|
||||
#include <Base/Tools.h>
|
||||
|
||||
#include <QColor>
|
||||
#include <QRegularExpression>
|
||||
#include <QString>
|
||||
#include <ranges>
|
||||
#include <algorithm>
|
||||
#include <variant>
|
||||
|
||||
namespace Gui::StyleParameters
|
||||
@@ -53,29 +51,20 @@ Value Color::evaluate([[maybe_unused]] const EvaluationContext& context) const
|
||||
|
||||
Value FunctionCall::evaluate(const EvaluationContext& context) const
|
||||
{
|
||||
const auto lightenOrDarken = [this](const EvaluationContext& context) -> Value {
|
||||
if (arguments.size() != 2) {
|
||||
THROWM(
|
||||
Base::ExpressionError,
|
||||
fmt::format("Function '{}' expects 2 arguments, got {}", functionName, arguments.size())
|
||||
);
|
||||
}
|
||||
auto argsValue = arguments.evaluate(context);
|
||||
const auto& args = argsValue.get<Tuple>();
|
||||
|
||||
auto colorArg = arguments[0]->evaluate(context);
|
||||
auto amountArg = arguments[1]->evaluate(context);
|
||||
const auto lightenOrDarken = [this, &args]() -> Value {
|
||||
auto resolved = ArgumentParser {{"color"}, {"amount"}}.resolve(args);
|
||||
|
||||
if (!std::holds_alternative<Base::Color>(colorArg)) {
|
||||
THROWM(Base::ExpressionError, fmt::format("'{}' is not supported for colors", functionName));
|
||||
}
|
||||
|
||||
auto color = std::get<Base::Color>(colorArg).asValue<QColor>();
|
||||
auto color = resolved.get<Base::Color>("color").asValue<QColor>();
|
||||
|
||||
// In Qt if you want to make color 20% darker or lighter, you need to pass 120 as the value
|
||||
// we, however, want users to pass only the relative difference, hence we need to add the
|
||||
// 100 required by Qt.
|
||||
//
|
||||
// NOLINTNEXTLINE(*-magic-numbers)
|
||||
auto amount = 100 + static_cast<int>(std::get<Numeric>(amountArg).value);
|
||||
auto amount = 100 + static_cast<int>(resolved.get<Numeric>("amount").value);
|
||||
|
||||
if (functionName == "lighten") {
|
||||
return Base::Color::fromValue(color.lighter(amount));
|
||||
@@ -88,36 +77,12 @@ Value FunctionCall::evaluate(const EvaluationContext& context) const
|
||||
return {};
|
||||
};
|
||||
|
||||
const auto blend = [this](const EvaluationContext& context) -> Value {
|
||||
if (arguments.size() != 3) {
|
||||
THROWM(
|
||||
Base::ExpressionError,
|
||||
fmt::format("Function '{}' expects 3 arguments, got {}", functionName, arguments.size())
|
||||
);
|
||||
}
|
||||
const auto blend = [&args]() -> Value {
|
||||
auto resolved = ArgumentParser {{"from"}, {"to"}, {"amount"}}.resolve(args);
|
||||
|
||||
auto firstColorArg = arguments[0]->evaluate(context);
|
||||
auto secondColorArg = arguments[1]->evaluate(context);
|
||||
auto amountArg = arguments[2]->evaluate(context);
|
||||
|
||||
if (!std::holds_alternative<Base::Color>(firstColorArg)) {
|
||||
THROWM(
|
||||
Base::ExpressionError,
|
||||
fmt::format("first argument of '{}' must be color", functionName)
|
||||
);
|
||||
}
|
||||
|
||||
if (!std::holds_alternative<Base::Color>(secondColorArg)) {
|
||||
THROWM(
|
||||
Base::ExpressionError,
|
||||
fmt::format("second argument of '{}' must be color", functionName)
|
||||
);
|
||||
}
|
||||
|
||||
auto firstColor = std::get<Base::Color>(firstColorArg);
|
||||
auto secondColor = std::get<Base::Color>(secondColorArg);
|
||||
|
||||
auto amount = Base::fromPercent(static_cast<long>(std::get<Numeric>(amountArg).value));
|
||||
auto firstColor = resolved.get<Base::Color>("from");
|
||||
auto secondColor = resolved.get<Base::Color>("to");
|
||||
auto amount = Base::fromPercent(static_cast<long>(resolved.get<Numeric>("amount").value));
|
||||
|
||||
return Base::Color(
|
||||
(1 - amount) * firstColor.r + amount * secondColor.r,
|
||||
@@ -126,15 +91,14 @@ Value FunctionCall::evaluate(const EvaluationContext& context) const
|
||||
);
|
||||
};
|
||||
|
||||
std::map<std::string, std::function<Value(const EvaluationContext&)>> functions = {
|
||||
std::map<std::string, std::function<Value()>> functions = {
|
||||
{"lighten", lightenOrDarken},
|
||||
{"darken", lightenOrDarken},
|
||||
{"blend", blend},
|
||||
};
|
||||
|
||||
if (functions.contains(functionName)) {
|
||||
auto function = functions.at(functionName);
|
||||
return function(context);
|
||||
return functions.at(functionName)();
|
||||
}
|
||||
|
||||
THROWM(Base::ExpressionError, fmt::format("Unknown function '{}'", functionName));
|
||||
@@ -145,45 +109,65 @@ Value BinaryOp::evaluate(const EvaluationContext& context) const
|
||||
Value lval = left->evaluate(context);
|
||||
Value rval = right->evaluate(context);
|
||||
|
||||
if (!std::holds_alternative<Numeric>(lval) || !std::holds_alternative<Numeric>(rval)) {
|
||||
THROWM(Base::ExpressionError, "Math operations are supported only on lengths");
|
||||
}
|
||||
|
||||
auto lhs = std::get<Numeric>(lval);
|
||||
auto rhs = std::get<Numeric>(rval);
|
||||
|
||||
switch (op) {
|
||||
case Operator::Add:
|
||||
return lhs + rhs;
|
||||
return lval + rval;
|
||||
case Operator::Subtract:
|
||||
return lhs - rhs;
|
||||
return lval - rval;
|
||||
case Operator::Multiply:
|
||||
return lhs * rhs;
|
||||
return lval * rval;
|
||||
case Operator::Divide:
|
||||
return lhs / rhs;
|
||||
return lval / rval;
|
||||
default:
|
||||
THROWM(Base::ExpressionError, "Unknown operator");
|
||||
}
|
||||
}
|
||||
|
||||
Value TupleLiteral::evaluate(const EvaluationContext& context) const
|
||||
{
|
||||
Tuple tuple;
|
||||
for (const auto& elem : elements) {
|
||||
tuple.elements.push_back(
|
||||
{elem.name, std::make_shared<const Value>(elem.expression->evaluate(context))}
|
||||
);
|
||||
}
|
||||
return tuple;
|
||||
}
|
||||
|
||||
Value UnaryOp::evaluate(const EvaluationContext& context) const
|
||||
{
|
||||
Value val = operand->evaluate(context);
|
||||
if (std::holds_alternative<Base::Color>(val)) {
|
||||
THROWM(Base::ExpressionError, "Unary operations on colors are not supported");
|
||||
}
|
||||
|
||||
auto v = std::get<Numeric>(val);
|
||||
switch (op) {
|
||||
case Operator::Add:
|
||||
return v;
|
||||
return val;
|
||||
case Operator::Subtract:
|
||||
return -v;
|
||||
return -val;
|
||||
default:
|
||||
THROWM(Base::ExpressionError, "Unknown unary operator");
|
||||
}
|
||||
}
|
||||
|
||||
Value MemberAccess::evaluate(const EvaluationContext& context) const
|
||||
{
|
||||
Value val = object->evaluate(context);
|
||||
if (!val.holds<Tuple>()) {
|
||||
THROWM(Base::ExpressionError, "Member access requires a tuple");
|
||||
}
|
||||
|
||||
const auto& tuple = val.get<Tuple>();
|
||||
|
||||
if (const Value* found = tuple.find(member)) {
|
||||
return *found;
|
||||
}
|
||||
|
||||
if (std::ranges::all_of(member, ::isdigit)) {
|
||||
return tuple.at(std::stoul(member));
|
||||
}
|
||||
|
||||
THROWM(Base::ExpressionError, fmt::format("Tuple has no member '{}'", member));
|
||||
}
|
||||
|
||||
std::unique_ptr<Expr> Parser::parse()
|
||||
{
|
||||
auto expr = parseExpression();
|
||||
@@ -245,23 +229,54 @@ std::unique_ptr<Expr> Parser::parseFactor()
|
||||
Operator op = (input[pos - 1] == '+') ? Operator::Add : Operator::Subtract;
|
||||
return std::make_unique<UnaryOp>(op, parseFactor());
|
||||
}
|
||||
|
||||
std::unique_ptr<Expr> expr;
|
||||
|
||||
if (match('(')) {
|
||||
auto expr = parseExpression();
|
||||
if (!match(')')) {
|
||||
THROWM(Base::ParserError, fmt::format("Expected ')', got '{}'", input[pos]));
|
||||
// Disambiguation: tuple vs grouped expression
|
||||
// 1. If we see `identifier:` pattern → definitely a tuple
|
||||
if (peekNamedElement()) {
|
||||
expr = parseTuple();
|
||||
}
|
||||
else {
|
||||
// 2. Otherwise parse first expression
|
||||
expr = parseExpression();
|
||||
skipWhitespace();
|
||||
|
||||
// If followed by `,` → reinterpret as tuple with this as first element
|
||||
if (pos < input.size() && input[pos] == ',') {
|
||||
++pos;
|
||||
TupleLiteral::Element first;
|
||||
first.expression = std::move(expr);
|
||||
expr = parseTuple(std::move(first));
|
||||
}
|
||||
else {
|
||||
// If followed by `)` → grouped expression (backward compatible)
|
||||
if (!match(')')) {
|
||||
THROWM(Base::ParserError, fmt::format("Expected ')', got '{}'", input[pos]));
|
||||
}
|
||||
}
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
if (peekColor()) {
|
||||
return parseColor();
|
||||
else if (peekColor()) {
|
||||
expr = parseColor();
|
||||
}
|
||||
if (peekParameter()) {
|
||||
return parseParameter();
|
||||
else if (peekParameter()) {
|
||||
expr = parseParameter();
|
||||
}
|
||||
if (peekFunction()) {
|
||||
return parseFunctionCall();
|
||||
else if (peekFunction()) {
|
||||
expr = parseFunctionCall();
|
||||
}
|
||||
return parseNumber();
|
||||
else {
|
||||
expr = parseNumber();
|
||||
}
|
||||
|
||||
while (pos < input.size() && input[pos] == '.') {
|
||||
++pos;
|
||||
expr = std::make_unique<MemberAccess>(std::move(expr), parseMember());
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
bool Parser::peekColor()
|
||||
@@ -386,21 +401,84 @@ std::unique_ptr<Expr> Parser::parseFunctionCall()
|
||||
THROWM(Base::ParserError, fmt::format("Expected '(' after function name, got '{}'", input[pos]));
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<Expr>> arguments;
|
||||
if (!match(')')) {
|
||||
do { // NOLINT(*-avoid-do-while)
|
||||
arguments.push_back(parseExpression());
|
||||
} while (match(','));
|
||||
auto arguments = parseTuple();
|
||||
return std::make_unique<FunctionCall>(functionName, std::move(*arguments));
|
||||
}
|
||||
|
||||
if (!match(')')) {
|
||||
THROWM(
|
||||
Base::ParserError,
|
||||
fmt::format("Expected ')' after function arguments, got '{}'", input[pos])
|
||||
);
|
||||
}
|
||||
bool Parser::peekNamedElement()
|
||||
{
|
||||
size_t saved = pos;
|
||||
skipWhitespace();
|
||||
|
||||
// Check for `identifier :` pattern
|
||||
if (pos >= input.size() || !isalpha(input[pos])) {
|
||||
pos = saved;
|
||||
return false;
|
||||
}
|
||||
|
||||
return std::make_unique<FunctionCall>(functionName, std::move(arguments));
|
||||
// Skip identifier characters
|
||||
while (pos < input.size() && (isalnum(input[pos]) || input[pos] == '_')) {
|
||||
++pos;
|
||||
}
|
||||
|
||||
// Skip whitespace between identifier and colon
|
||||
while (pos < input.size() && isspace(input[pos])) {
|
||||
++pos;
|
||||
}
|
||||
|
||||
// Check for colon
|
||||
bool found = pos < input.size() && input[pos] == ':';
|
||||
pos = saved;
|
||||
return found;
|
||||
}
|
||||
|
||||
std::unique_ptr<TupleLiteral> Parser::parseTuple(std::optional<TupleLiteral::Element> firstElement)
|
||||
{
|
||||
auto tuple = std::make_unique<TupleLiteral>();
|
||||
|
||||
const auto parseElement = [this]() {
|
||||
TupleLiteral::Element elem;
|
||||
|
||||
// Check if this element has a name
|
||||
if (peekNamedElement()) {
|
||||
skipWhitespace();
|
||||
size_t start = pos;
|
||||
while (pos < input.size() && (isalnum(input[pos]) || input[pos] == '_')) {
|
||||
++pos;
|
||||
}
|
||||
elem.name = input.substr(start, pos - start);
|
||||
skipWhitespace();
|
||||
++pos; // consume ':'
|
||||
}
|
||||
|
||||
elem.expression = parseExpression();
|
||||
return elem;
|
||||
};
|
||||
|
||||
if (firstElement) {
|
||||
// Called from unnamed-element path: first element already parsed, comma already consumed
|
||||
tuple->elements.push_back(std::move(*firstElement));
|
||||
// Parse the second element immediately (comma was already consumed by caller)
|
||||
tuple->elements.push_back(parseElement());
|
||||
}
|
||||
|
||||
// Parse remaining elements
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (pos < input.size() && input[pos] == ')') {
|
||||
++pos;
|
||||
return tuple;
|
||||
}
|
||||
|
||||
if (!tuple->elements.empty() && !match(',')) {
|
||||
if (pos >= input.size()) {
|
||||
THROWM(Base::ParserError, "Expected ')' to close tuple");
|
||||
}
|
||||
THROWM(Base::ParserError, fmt::format("Expected ',' or ')' in tuple, got '{}'", input[pos]));
|
||||
}
|
||||
|
||||
tuple->elements.push_back(parseElement());
|
||||
}
|
||||
}
|
||||
|
||||
int Parser::parseInt()
|
||||
@@ -446,6 +524,18 @@ std::string Parser::parseUnit()
|
||||
return input.substr(start, pos - start);
|
||||
}
|
||||
|
||||
std::string Parser::parseMember()
|
||||
{
|
||||
size_t start = pos;
|
||||
while (pos < input.size() && (isalnum(input[pos]) || input[pos] == '_')) {
|
||||
++pos;
|
||||
}
|
||||
if (start == pos) {
|
||||
THROWM(Base::ParserError, "Expected member name after '.'");
|
||||
}
|
||||
return input.substr(start, pos - start);
|
||||
}
|
||||
|
||||
bool Parser::match(char expected)
|
||||
{
|
||||
skipWhitespace();
|
||||
|
||||
@@ -91,12 +91,25 @@ struct GuiExport Color: public Expr
|
||||
Value evaluate([[maybe_unused]] const EvaluationContext& context) const override;
|
||||
};
|
||||
|
||||
struct GuiExport TupleLiteral: public Expr
|
||||
{
|
||||
struct Element
|
||||
{
|
||||
std::optional<std::string> name;
|
||||
std::unique_ptr<Expr> expression;
|
||||
};
|
||||
|
||||
std::vector<Element> elements;
|
||||
|
||||
Value evaluate(const EvaluationContext& context) const override;
|
||||
};
|
||||
|
||||
struct GuiExport FunctionCall: public Expr
|
||||
{
|
||||
std::string functionName;
|
||||
std::vector<std::unique_ptr<Expr>> arguments;
|
||||
TupleLiteral arguments;
|
||||
|
||||
FunctionCall(std::string functionName, std::vector<std::unique_ptr<Expr>> arguments)
|
||||
FunctionCall(std::string functionName, TupleLiteral arguments)
|
||||
: functionName(std::move(functionName))
|
||||
, arguments(std::move(arguments))
|
||||
{}
|
||||
@@ -131,6 +144,19 @@ struct GuiExport UnaryOp: public Expr
|
||||
Value evaluate(const EvaluationContext& context) const override;
|
||||
};
|
||||
|
||||
struct GuiExport MemberAccess: public Expr
|
||||
{
|
||||
std::unique_ptr<Expr> object;
|
||||
std::string member;
|
||||
|
||||
MemberAccess(std::unique_ptr<Expr> object, std::string member)
|
||||
: object(std::move(object))
|
||||
, member(std::move(member))
|
||||
{}
|
||||
|
||||
Value evaluate(const EvaluationContext& context) const override;
|
||||
};
|
||||
|
||||
class GuiExport Parser
|
||||
{
|
||||
static constexpr auto rgbFunction = "rgb(";
|
||||
@@ -160,6 +186,11 @@ private:
|
||||
int parseInt();
|
||||
std::unique_ptr<Expr> parseNumber();
|
||||
std::string parseUnit();
|
||||
std::string parseMember();
|
||||
bool peekNamedElement();
|
||||
std::unique_ptr<TupleLiteral> parseTuple(
|
||||
std::optional<TupleLiteral::Element> firstElement = std::nullopt
|
||||
);
|
||||
bool match(char expected);
|
||||
void skipWhitespace();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
/****************************************************************************
|
||||
* *
|
||||
* Copyright (c) 2025 Kacper Donat <[email protected]> *
|
||||
* *
|
||||
* This file is part of FreeCAD. *
|
||||
* *
|
||||
* FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
* under the terms of the GNU Lesser General Public License as *
|
||||
* published by the Free Software Foundation, either version 2.1 of the *
|
||||
* License, or (at your option) any later version. *
|
||||
* *
|
||||
* FreeCAD is distributed in the hope that it will be useful, but *
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
* Lesser General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Lesser General Public *
|
||||
* License along with FreeCAD. If not, see *
|
||||
* <https://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#include "Value.h"
|
||||
|
||||
#include <functional>
|
||||
#include <ranges>
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
namespace Gui::StyleParameters
|
||||
{
|
||||
|
||||
Numeric Numeric::operator+(const Numeric& rhs) const
|
||||
{
|
||||
ensureEqualUnits(rhs);
|
||||
return {value + rhs.value, unit};
|
||||
}
|
||||
|
||||
Numeric Numeric::operator-(const Numeric& rhs) const
|
||||
{
|
||||
ensureEqualUnits(rhs);
|
||||
return {value - rhs.value, unit};
|
||||
}
|
||||
|
||||
Numeric Numeric::operator-() const
|
||||
{
|
||||
return {-value, unit};
|
||||
}
|
||||
|
||||
Numeric Numeric::operator/(const Numeric& rhs) const
|
||||
{
|
||||
if (rhs.value == 0) {
|
||||
THROWM(Base::RuntimeError, "Division by zero");
|
||||
}
|
||||
|
||||
if (rhs.unit.empty() || unit.empty()) {
|
||||
return {value / rhs.value, unit};
|
||||
}
|
||||
|
||||
ensureEqualUnits(rhs);
|
||||
return {value / rhs.value, unit};
|
||||
}
|
||||
|
||||
Numeric Numeric::operator*(const Numeric& rhs) const
|
||||
{
|
||||
if (rhs.unit.empty() || unit.empty()) {
|
||||
return {value * rhs.value, unit};
|
||||
}
|
||||
|
||||
ensureEqualUnits(rhs);
|
||||
return {value * rhs.value, unit};
|
||||
}
|
||||
|
||||
void Numeric::ensureEqualUnits(const Numeric& rhs) const
|
||||
{
|
||||
if (unit != rhs.unit) {
|
||||
THROWM(
|
||||
Base::RuntimeError,
|
||||
fmt::format("Units mismatch left expression is '{}', right expression is '{}'", unit, rhs.unit)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
std::string Value::toString() const
|
||||
{
|
||||
if (holds<Numeric>()) {
|
||||
auto [value, unit] = get<Numeric>();
|
||||
return fmt::format("{}{}", value, unit);
|
||||
}
|
||||
|
||||
if (holds<Base::Color>()) {
|
||||
auto color = get<Base::Color>();
|
||||
return fmt::format("#{:0>6x}", color.getPackedRGB() >> 8); // NOLINT(*-magic-numbers)
|
||||
}
|
||||
|
||||
if (holds<Tuple>()) {
|
||||
const auto& tuple = get<Tuple>();
|
||||
|
||||
std::vector<std::string> parts;
|
||||
parts.reserve(tuple.elements.size());
|
||||
|
||||
for (const auto& [name, value] : tuple.elements) {
|
||||
std::string string;
|
||||
|
||||
if (name) {
|
||||
string = fmt::format("{}: {}", *name, value->toString());
|
||||
}
|
||||
else {
|
||||
string = value->toString();
|
||||
}
|
||||
|
||||
parts.push_back(std::move(string));
|
||||
}
|
||||
|
||||
return fmt::format("({})", fmt::join(parts, ", "));
|
||||
}
|
||||
|
||||
return get<std::string>();
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
Tuple elementWise(const Tuple& lhs, const Tuple& rhs, auto op)
|
||||
{
|
||||
Tuple result;
|
||||
std::vector<bool> rhsUsed(rhs.size(), false);
|
||||
|
||||
// Phase 1: LHS named elements — match by name in RHS
|
||||
for (size_t i = 0; i < lhs.size(); ++i) {
|
||||
if (!lhs.elements[i].name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto& name = *lhs.elements[i].name;
|
||||
bool matched = false;
|
||||
|
||||
for (size_t j = 0; j < rhs.size(); ++j) {
|
||||
if (!rhsUsed[j] && rhs.elements[j].name && *rhs.elements[j].name == name) {
|
||||
rhsUsed[j] = true;
|
||||
matched = true;
|
||||
result.elements.emplace_back(
|
||||
lhs.elements[i].name,
|
||||
std::make_shared<const Value>(op(lhs.at(i), rhs.at(j)))
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
result.elements.push_back(lhs.elements[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: RHS named elements not yet matched
|
||||
for (size_t j = 0; j < rhs.size(); ++j) {
|
||||
if (rhsUsed[j] || !rhs.elements[j].name) {
|
||||
continue;
|
||||
}
|
||||
rhsUsed[j] = true;
|
||||
result.elements.push_back(rhs.elements[j]);
|
||||
}
|
||||
|
||||
// Phase 3: Unnamed elements — match positionally
|
||||
std::vector<size_t> lhsUnnamed;
|
||||
std::vector<size_t> rhsUnnamed;
|
||||
|
||||
for (size_t i = 0; i < lhs.size(); ++i) {
|
||||
if (!lhs.elements[i].name) {
|
||||
lhsUnnamed.push_back(i);
|
||||
}
|
||||
}
|
||||
for (size_t j = 0; j < rhs.size(); ++j) {
|
||||
if (!rhsUsed[j]) {
|
||||
rhsUnnamed.push_back(j);
|
||||
}
|
||||
}
|
||||
|
||||
size_t paired = std::min(lhsUnnamed.size(), rhsUnnamed.size());
|
||||
for (size_t k = 0; k < paired; ++k) {
|
||||
result.elements.emplace_back(
|
||||
std::nullopt,
|
||||
std::make_shared<const Value>(op(lhs.at(lhsUnnamed[k]), rhs.at(rhsUnnamed[k])))
|
||||
);
|
||||
}
|
||||
for (size_t k = paired; k < lhsUnnamed.size(); ++k) {
|
||||
result.elements.push_back(lhs.elements[lhsUnnamed[k]]);
|
||||
}
|
||||
for (size_t k = paired; k < rhsUnnamed.size(); ++k) {
|
||||
result.elements.push_back(rhs.elements[rhsUnnamed[k]]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Tuple scalarBroadcast(const Tuple& tuple, const Value& scalar, auto op)
|
||||
{
|
||||
Tuple result;
|
||||
for (size_t i = 0; i < tuple.size(); ++i) {
|
||||
result.elements.emplace_back(
|
||||
tuple.elements[i].name,
|
||||
std::make_shared<const Value>(op(tuple.at(i), scalar))
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Value Value::operator+(const Value& rhs) const
|
||||
{
|
||||
if (holds<Numeric>() && rhs.holds<Numeric>()) {
|
||||
return get<Numeric>() + rhs.get<Numeric>();
|
||||
}
|
||||
if (holds<Tuple>() && rhs.holds<Tuple>()) {
|
||||
return elementWise(get<Tuple>(), rhs.get<Tuple>(), std::plus<Value> {});
|
||||
}
|
||||
THROWM(Base::ExpressionError, "Addition requires two numerics or two tuples of equal size");
|
||||
}
|
||||
|
||||
Value Value::operator-(const Value& rhs) const
|
||||
{
|
||||
if (holds<Numeric>() && rhs.holds<Numeric>()) {
|
||||
return get<Numeric>() - rhs.get<Numeric>();
|
||||
}
|
||||
if (holds<Tuple>() && rhs.holds<Tuple>()) {
|
||||
return elementWise(get<Tuple>(), rhs.get<Tuple>(), std::minus<Value> {});
|
||||
}
|
||||
THROWM(Base::ExpressionError, "Subtraction requires two numerics or two tuples of equal size");
|
||||
}
|
||||
|
||||
Value Value::operator*(const Value& rhs) const
|
||||
{
|
||||
if (holds<Numeric>() && rhs.holds<Numeric>()) {
|
||||
return get<Numeric>() * rhs.get<Numeric>();
|
||||
}
|
||||
if (holds<Tuple>() && rhs.holds<Numeric>()) {
|
||||
return scalarBroadcast(get<Tuple>(), rhs, std::multiplies<Value> {});
|
||||
}
|
||||
if (holds<Numeric>() && rhs.holds<Tuple>()) {
|
||||
return scalarBroadcast(rhs.get<Tuple>(), *this, [](const Value& elem, const Value& scalar) {
|
||||
return elem * scalar;
|
||||
});
|
||||
}
|
||||
THROWM(Base::ExpressionError, "Multiplication requires two numerics or tuple with scalar");
|
||||
}
|
||||
|
||||
Value Value::operator/(const Value& rhs) const
|
||||
{
|
||||
if (holds<Numeric>() && rhs.holds<Numeric>()) {
|
||||
return get<Numeric>() / rhs.get<Numeric>();
|
||||
}
|
||||
if (holds<Tuple>() && rhs.holds<Numeric>()) {
|
||||
return scalarBroadcast(get<Tuple>(), rhs, std::divides<Value> {});
|
||||
}
|
||||
THROWM(Base::ExpressionError, "Division requires two numerics or tuple divided by scalar");
|
||||
}
|
||||
|
||||
Value Value::operator-() const
|
||||
{
|
||||
if (holds<Numeric>()) {
|
||||
return -get<Numeric>();
|
||||
}
|
||||
if (holds<Tuple>()) {
|
||||
Tuple result;
|
||||
const auto& tuple = get<Tuple>();
|
||||
for (size_t i = 0; i < tuple.size(); ++i) {
|
||||
result.elements.emplace_back(
|
||||
tuple.elements[i].name,
|
||||
std::make_shared<const Value>(-tuple.at(i))
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
THROWM(Base::ExpressionError, "Unary negation requires a numeric or tuple value");
|
||||
}
|
||||
|
||||
const Value& Tuple::at(size_t index) const
|
||||
{
|
||||
if (index >= elements.size()) {
|
||||
THROWM(
|
||||
Base::RuntimeError,
|
||||
fmt::format("Tuple index {} out of range (size {})", index, elements.size())
|
||||
);
|
||||
}
|
||||
return *elements[index].value;
|
||||
}
|
||||
|
||||
const Value* Tuple::find(const std::string& name) const
|
||||
{
|
||||
auto it = std::ranges::find_if(elements, [&](const Element& elem) {
|
||||
return elem.name && *elem.name == name;
|
||||
});
|
||||
|
||||
return it != elements.end() ? it->value.get() : nullptr;
|
||||
}
|
||||
|
||||
size_t Tuple::size() const
|
||||
{
|
||||
return elements.size();
|
||||
}
|
||||
|
||||
ArgumentParser::ArgumentParser(std::initializer_list<ParamDef> params)
|
||||
: params_(params)
|
||||
{}
|
||||
|
||||
Tuple ArgumentParser::resolve(const Tuple& args) const
|
||||
{
|
||||
// Slots for resolved values, one per declared parameter
|
||||
std::vector<std::shared_ptr<const Value>> slots(params_.size());
|
||||
|
||||
// 1. Match named arguments to their declared parameter
|
||||
for (const auto& elem : args.elements) {
|
||||
if (!elem.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto it = std::ranges::find_if(params_, [&](const ParamDef& p) {
|
||||
return p.name == *elem.name;
|
||||
});
|
||||
|
||||
if (it == params_.end()) {
|
||||
THROWM(Base::ExpressionError, fmt::format("Unknown argument '{}'", *elem.name));
|
||||
}
|
||||
|
||||
auto index = static_cast<size_t>(std::distance(params_.begin(), it));
|
||||
|
||||
if (slots[index]) {
|
||||
THROWM(Base::ExpressionError, fmt::format("Duplicate argument '{}'", *elem.name));
|
||||
}
|
||||
|
||||
slots[index] = elem.value;
|
||||
}
|
||||
|
||||
// 2. Fill remaining slots with unnamed arguments in order
|
||||
auto unnamed = args.elements
|
||||
| std::views::filter([](const Tuple::Element& e) { return !e.name.has_value(); });
|
||||
auto unnamedIt = unnamed.begin();
|
||||
|
||||
for (size_t i = 0; i < params_.size(); ++i) {
|
||||
if (slots[i]) {
|
||||
continue; // already claimed by name
|
||||
}
|
||||
|
||||
if (unnamedIt != unnamed.end()) {
|
||||
slots[i] = unnamedIt->value;
|
||||
++unnamedIt;
|
||||
}
|
||||
else if (params_[i].defaultValue) {
|
||||
slots[i] = std::make_shared<const Value>(*params_[i].defaultValue);
|
||||
}
|
||||
else {
|
||||
THROWM(
|
||||
Base::ExpressionError,
|
||||
fmt::format("Missing required argument '{}'", params_[i].name)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check for excess positional arguments
|
||||
if (unnamedIt != unnamed.end()) {
|
||||
THROWM(
|
||||
Base::ExpressionError,
|
||||
fmt::format("Too many arguments: expected {}, got {}", params_.size(), args.size())
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Build result Tuple with all elements named per signature
|
||||
Tuple result;
|
||||
for (size_t i = 0; i < params_.size(); ++i) {
|
||||
result.elements.emplace_back(params_[i].name, std::move(slots[i]));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Gui::StyleParameters
|
||||
@@ -0,0 +1,276 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
/****************************************************************************
|
||||
* *
|
||||
* Copyright (c) 2025 Kacper Donat <[email protected]> *
|
||||
* *
|
||||
* This file is part of FreeCAD. *
|
||||
* *
|
||||
* FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
* under the terms of the GNU Lesser General Public License as *
|
||||
* published by the Free Software Foundation, either version 2.1 of the *
|
||||
* License, or (at your option) any later version. *
|
||||
* *
|
||||
* FreeCAD is distributed in the hope that it will be useful, but *
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
* Lesser General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Lesser General Public *
|
||||
* License along with FreeCAD. If not, see *
|
||||
* <https://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef STYLEPARAMETERS_VALUE_H
|
||||
#define STYLEPARAMETERS_VALUE_H
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <Base/Color.h>
|
||||
#include <Base/Exception.h>
|
||||
#include <FCGlobal.h>
|
||||
|
||||
namespace Gui::StyleParameters
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Represents a length in a specified unit.
|
||||
*
|
||||
* This struct is a very simplified representation of lengths that can be used as parameters for
|
||||
* styling purposes. The length basically consists of value and unit. Unit is optional, empty unit
|
||||
* represents a dimensionless length that can be used as a scalar. This struct does not care about
|
||||
* unit conversions as its uses do not require it.
|
||||
*/
|
||||
struct GuiExport Numeric
|
||||
{
|
||||
/// Numeric value of the length.
|
||||
double value;
|
||||
/// Unit of the length, empty if the value is dimensionless.
|
||||
std::string unit = "";
|
||||
|
||||
/**
|
||||
* @name Operators
|
||||
*
|
||||
* This struct supports basic operations on Length. Each operation requires for operands to be
|
||||
* the same unit. Multiplication and division additionally allow one operand to be dimensionless
|
||||
* and hence act as a scalar.
|
||||
*
|
||||
* @code{c++}
|
||||
* Numeric a { 10, "px" };
|
||||
* Numeric b { 5, "px" };
|
||||
*
|
||||
* Numeric differentUnit { 3, "rem" }
|
||||
* Numeric scalar { 2, "" };
|
||||
*
|
||||
* // basic operations with the same unit are allowed
|
||||
* auto sum = a + b; // 15 px
|
||||
* auto difference = a - 5; // 10 px
|
||||
*
|
||||
* // basic operations with mixed units are NOT allowed
|
||||
* auto sumOfIncompatibleUnits = a + differentUnit; // will throw
|
||||
* auto productOfIncompatibleUnits = a * differentUnit; // will throw
|
||||
*
|
||||
* // exception is that for multiplication and division dimensionless units are allowed
|
||||
* auto productWithScalar = a * scalar; // 20 px
|
||||
* @endcode
|
||||
* @{
|
||||
*/
|
||||
Numeric operator+(const Numeric& rhs) const;
|
||||
Numeric operator-(const Numeric& rhs) const;
|
||||
Numeric operator-() const;
|
||||
|
||||
Numeric operator/(const Numeric& rhs) const;
|
||||
Numeric operator*(const Numeric& rhs) const;
|
||||
/// @}
|
||||
|
||||
private:
|
||||
void ensureEqualUnits(const Numeric& rhs) const;
|
||||
};
|
||||
|
||||
// Forward declaration: Tuple::Element uses shared_ptr<const Value> to break the
|
||||
// circular dependency (Value contains Tuple, Tuple elements contain Value).
|
||||
struct Value;
|
||||
|
||||
/**
|
||||
* @brief Represents a tuple of named or unnamed values.
|
||||
*
|
||||
* Tuples group related values into a single parameter using `(key: val1, val2)` syntax.
|
||||
* Elements can optionally have names for named access.
|
||||
*/
|
||||
struct GuiExport Tuple
|
||||
{
|
||||
struct Element
|
||||
{
|
||||
std::optional<std::string> name;
|
||||
std::shared_ptr<const Value> value;
|
||||
};
|
||||
|
||||
std::vector<Element> elements;
|
||||
|
||||
/**
|
||||
* @brief Returns the value at the given index (bounds-checked).
|
||||
* @throws Base::RuntimeError if index is out of range.
|
||||
*/
|
||||
const Value& at(size_t index) const;
|
||||
|
||||
/**
|
||||
* @brief Finds an element by name.
|
||||
* @return Pointer to the value if found, nullptr otherwise.
|
||||
*/
|
||||
const Value* find(const std::string& name) const;
|
||||
|
||||
/**
|
||||
* @brief Returns the number of elements in the tuple.
|
||||
*/
|
||||
size_t size() const;
|
||||
|
||||
/**
|
||||
* @brief Gets a named element with type checking and user-friendly error messages.
|
||||
*
|
||||
* @throws Base::ExpressionError if the element is not found or has the wrong type.
|
||||
*/
|
||||
template<typename T>
|
||||
const T& get(const std::string& name) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief This struct represents any valid value that can be used as the parameter value.
|
||||
*
|
||||
* The value can be one of four basic types:
|
||||
* - Numbers / Lengths (so any length with optional unit) (Numeric)
|
||||
* - Colors (Base::Color)
|
||||
* - Any other generic expression (std::string)
|
||||
* - Tuples of values (Tuple)
|
||||
*
|
||||
* As a rule, operations can be only performed over values of the same type.
|
||||
*/
|
||||
struct GuiExport Value: std::variant<Numeric, Base::Color, std::string, Tuple>
|
||||
{
|
||||
using std::variant<Numeric, Base::Color, std::string, Tuple>::variant;
|
||||
|
||||
/**
|
||||
* Converts the object into its string representation.
|
||||
*
|
||||
* @return A string representation of the object that can later be used in QSS.
|
||||
*/
|
||||
std::string toString() const;
|
||||
|
||||
/**
|
||||
* @brief Checks whether this value holds the given type.
|
||||
*/
|
||||
template<typename T>
|
||||
bool holds() const
|
||||
{
|
||||
return std::holds_alternative<T>(*this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gets the value of the given type.
|
||||
*
|
||||
* Throws std::bad_variant_access if the value does not hold the requested type.
|
||||
*/
|
||||
template<typename T>
|
||||
const T& get() const
|
||||
{
|
||||
return std::get<T>(*this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Arithmetic operators
|
||||
*
|
||||
* Element-wise operations for tuples of equal size, scalar broadcast for Tuple × Numeric.
|
||||
* Delegates to Numeric operators for Numeric × Numeric.
|
||||
* @{
|
||||
*/
|
||||
Value operator+(const Value& rhs) const;
|
||||
Value operator-(const Value& rhs) const;
|
||||
Value operator*(const Value& rhs) const;
|
||||
Value operator/(const Value& rhs) const;
|
||||
Value operator-() const;
|
||||
/// @}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
constexpr const char* valueTypeName()
|
||||
{
|
||||
if constexpr (std::is_same_v<T, Numeric>) {
|
||||
return "numeric";
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, Base::Color>) {
|
||||
return "color";
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::string>) {
|
||||
return "string";
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, Tuple>) {
|
||||
return "tuple";
|
||||
}
|
||||
|
||||
return "<unknown>";
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
const T& Tuple::get(const std::string& name) const
|
||||
{
|
||||
const Value* value = find(name);
|
||||
|
||||
if (!value) {
|
||||
THROWM(Base::ExpressionError, fmt::format("Missing argument '{}'", name));
|
||||
}
|
||||
|
||||
if (!value->holds<T>()) {
|
||||
THROWM(
|
||||
Base::ExpressionError,
|
||||
fmt::format("Argument '{}' must be {}, got {}", name, valueTypeName<T>(), value->toString())
|
||||
);
|
||||
}
|
||||
|
||||
return value->get<T>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Defines a single parameter in a function signature.
|
||||
*
|
||||
* Used with ArgumentParser to declare positional/named parameters with optional defaults.
|
||||
*/
|
||||
struct ParamDef
|
||||
{
|
||||
std::string name;
|
||||
std::optional<Value> defaultValue = {};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Resolves a Tuple of mixed positional/named arguments against a declared signature.
|
||||
*
|
||||
* Named arguments are matched by name, unnamed arguments fill remaining slots in declaration
|
||||
* order, and defaults fill any still-empty slots. The result is a Tuple with all elements named
|
||||
* per the signature.
|
||||
*
|
||||
* Example:
|
||||
* @code{.cpp}
|
||||
* auto resolved = ArgumentParser{{"color"}, {"amount", Numeric{20, ""}}}.resolve(args);
|
||||
* auto& color = resolved.get<Base::Color>("color");
|
||||
* auto& amount = resolved.get<Numeric>("amount");
|
||||
* @endcode
|
||||
*/
|
||||
class GuiExport ArgumentParser
|
||||
{
|
||||
public:
|
||||
ArgumentParser(std::initializer_list<ParamDef> params);
|
||||
|
||||
Tuple resolve(const Tuple& args) const;
|
||||
|
||||
private:
|
||||
std::vector<ParamDef> params_;
|
||||
};
|
||||
|
||||
} // namespace Gui::StyleParameters
|
||||
|
||||
#endif // STYLEPARAMETERS_VALUE_H
|
||||
@@ -41,6 +41,9 @@ protected:
|
||||
{"TestParam", "10px"},
|
||||
{"TestColor", "#ff0000"},
|
||||
{"TestNumber", "5"},
|
||||
{"TestTupleParam", "(left: 1px, right: 2px, top: 3px, bottom: 4px)"},
|
||||
{"TestIndexedTuple", "(10, 20, 30)"},
|
||||
{"TestNestedTuple", "((x: 1, y: 2), (x: 3, y: 4))"},
|
||||
},
|
||||
ParameterSource::Metadata {"Test Source"}
|
||||
);
|
||||
@@ -604,6 +607,268 @@ TEST_F(ParserTest, ParseOperatorPrecedence)
|
||||
}
|
||||
}
|
||||
|
||||
// Test unnamed tuple
|
||||
TEST_F(ParserTest, ParseUnnamedTuple)
|
||||
{
|
||||
Parser parser("(10, 20, 30)");
|
||||
auto expr = parser.parse();
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 3);
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(tuple.at(0)).value, 10.0);
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(tuple.at(1)).value, 20.0);
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(tuple.at(2)).value, 30.0);
|
||||
}
|
||||
|
||||
// Test named tuple
|
||||
TEST_F(ParserTest, ParseNamedTuple)
|
||||
{
|
||||
Parser parser("(x: 10, y: 20)");
|
||||
auto expr = parser.parse();
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 2);
|
||||
|
||||
auto* x = tuple.find("x");
|
||||
ASSERT_NE(x, nullptr);
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(*x).value, 10.0);
|
||||
|
||||
auto* y = tuple.find("y");
|
||||
ASSERT_NE(y, nullptr);
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(*y).value, 20.0);
|
||||
}
|
||||
|
||||
// Test mixed named/unnamed tuple
|
||||
TEST_F(ParserTest, ParseMixedTuple)
|
||||
{
|
||||
Parser parser("(x: 10, 20)");
|
||||
auto expr = parser.parse();
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 2);
|
||||
|
||||
auto* x = tuple.find("x");
|
||||
ASSERT_NE(x, nullptr);
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(*x).value, 10.0);
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(tuple.at(1)).value, 20.0);
|
||||
}
|
||||
|
||||
// Test single named element is a tuple, not a grouped expression
|
||||
TEST_F(ParserTest, ParseSingleNamedElementIsTuple)
|
||||
{
|
||||
Parser parser("(x: 10)");
|
||||
auto expr = parser.parse();
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 1);
|
||||
|
||||
auto* x = tuple.find("x");
|
||||
ASSERT_NE(x, nullptr);
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(*x).value, 10.0);
|
||||
}
|
||||
|
||||
// Test expressions inside tuple elements
|
||||
TEST_F(ParserTest, ParseTupleWithExpressions)
|
||||
{
|
||||
Parser parser("(x: 10 + 5, y: @TestParam)");
|
||||
auto expr = parser.parse();
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 2);
|
||||
|
||||
auto* x = tuple.find("x");
|
||||
ASSERT_NE(x, nullptr);
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(*x).value, 15.0);
|
||||
|
||||
auto* y = tuple.find("y");
|
||||
ASSERT_NE(y, nullptr);
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(*y).value, 10.0);
|
||||
EXPECT_EQ(std::get<Numeric>(*y).unit, "px");
|
||||
}
|
||||
|
||||
// Test mixed types in tuple
|
||||
TEST_F(ParserTest, ParseTupleWithMixedTypes)
|
||||
{
|
||||
Parser parser("(color: #ff0000, opacity: 0.5)");
|
||||
auto expr = parser.parse();
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 2);
|
||||
|
||||
auto* color = tuple.find("color");
|
||||
ASSERT_NE(color, nullptr);
|
||||
EXPECT_TRUE(std::holds_alternative<Base::Color>(*color));
|
||||
auto c = std::get<Base::Color>(*color);
|
||||
EXPECT_EQ(c.r, 1);
|
||||
EXPECT_EQ(c.g, 0);
|
||||
EXPECT_EQ(c.b, 0);
|
||||
|
||||
auto* opacity = tuple.find("opacity");
|
||||
ASSERT_NE(opacity, nullptr);
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(*opacity).value, 0.5);
|
||||
}
|
||||
|
||||
// Test nested tuples
|
||||
TEST_F(ParserTest, ParseNestedTuples)
|
||||
{
|
||||
Parser parser("((1, 2), (3, 4))");
|
||||
auto expr = parser.parse();
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
const auto& outer = result.get<Tuple>();
|
||||
EXPECT_EQ(outer.size(), 2);
|
||||
|
||||
EXPECT_TRUE(outer.at(0).holds<Tuple>());
|
||||
const auto& inner1 = outer.at(0).get<Tuple>();
|
||||
EXPECT_EQ(inner1.size(), 2);
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(inner1.at(0)).value, 1.0);
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(inner1.at(1)).value, 2.0);
|
||||
|
||||
EXPECT_TRUE(outer.at(1).holds<Tuple>());
|
||||
const auto& inner2 = outer.at(1).get<Tuple>();
|
||||
EXPECT_EQ(inner2.size(), 2);
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(inner2.at(0)).value, 3.0);
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(inner2.at(1)).value, 4.0);
|
||||
}
|
||||
|
||||
// Test complex expressions in tuple elements
|
||||
TEST_F(ParserTest, ParseTupleWithComplexExpressions)
|
||||
{
|
||||
Parser parser("(x: lighten(#ff0000, 20), y: 10px * 2)");
|
||||
auto expr = parser.parse();
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 2);
|
||||
|
||||
auto* x = tuple.find("x");
|
||||
ASSERT_NE(x, nullptr);
|
||||
EXPECT_TRUE(std::holds_alternative<Base::Color>(*x));
|
||||
|
||||
auto* y = tuple.find("y");
|
||||
ASSERT_NE(y, nullptr);
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(*y).value, 20.0);
|
||||
EXPECT_EQ(std::get<Numeric>(*y).unit, "px");
|
||||
}
|
||||
|
||||
// Test tuple toString roundtrip
|
||||
TEST_F(ParserTest, TupleToString)
|
||||
{
|
||||
{
|
||||
Parser parser("(x: 10, y: 20)");
|
||||
auto expr = parser.parse();
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
auto str = result.toString();
|
||||
EXPECT_EQ(str, "(x: 10, y: 20)");
|
||||
}
|
||||
|
||||
{
|
||||
Parser parser("(10, 20, 30)");
|
||||
auto expr = parser.parse();
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
auto str = result.toString();
|
||||
EXPECT_EQ(str, "(10, 20, 30)");
|
||||
}
|
||||
|
||||
{
|
||||
Parser parser("(x: 10, 20)");
|
||||
auto expr = parser.parse();
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
auto str = result.toString();
|
||||
EXPECT_EQ(str, "(x: 10, 20)");
|
||||
}
|
||||
}
|
||||
|
||||
// Test Value::holds and Value::get
|
||||
TEST_F(ParserTest, ValueHoldsAndGet)
|
||||
{
|
||||
Value numericValue = Numeric {42, "px"};
|
||||
EXPECT_TRUE(numericValue.holds<Numeric>());
|
||||
EXPECT_FALSE(numericValue.holds<Tuple>());
|
||||
EXPECT_DOUBLE_EQ(numericValue.get<Numeric>().value, 42.0);
|
||||
|
||||
Value colorValue = Base::Color(1.0, 0.0, 0.0);
|
||||
EXPECT_TRUE(colorValue.holds<Base::Color>());
|
||||
EXPECT_FALSE(colorValue.holds<Tuple>());
|
||||
|
||||
Value stringValue = std::string("hello");
|
||||
EXPECT_TRUE(stringValue.holds<std::string>());
|
||||
EXPECT_FALSE(stringValue.holds<Tuple>());
|
||||
|
||||
Parser parser("(1, 2)");
|
||||
auto expr = parser.parse();
|
||||
auto tupleValue = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(tupleValue.holds<Tuple>());
|
||||
EXPECT_FALSE(tupleValue.holds<Numeric>());
|
||||
EXPECT_EQ(tupleValue.get<Tuple>().size(), 2);
|
||||
}
|
||||
|
||||
// Test Tuple::at out of bounds
|
||||
TEST_F(ParserTest, TupleAtOutOfBounds)
|
||||
{
|
||||
Parser parser("(10, 20)");
|
||||
auto expr = parser.parse();
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
|
||||
EXPECT_NO_THROW(tuple.at(0));
|
||||
EXPECT_NO_THROW(tuple.at(1));
|
||||
EXPECT_THROW(tuple.at(2), Base::RuntimeError);
|
||||
}
|
||||
|
||||
// Test Tuple::find nonexistent name
|
||||
TEST_F(ParserTest, TupleFindNonexistent)
|
||||
{
|
||||
Parser parser("(x: 10, y: 20)");
|
||||
auto expr = parser.parse();
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
|
||||
EXPECT_NE(tuple.find("x"), nullptr);
|
||||
EXPECT_NE(tuple.find("y"), nullptr);
|
||||
EXPECT_EQ(tuple.find("z"), nullptr);
|
||||
}
|
||||
|
||||
// Test error: missing ')' in tuple
|
||||
TEST_F(ParserTest, TupleMissingClosingParen)
|
||||
{
|
||||
EXPECT_THROW(
|
||||
{
|
||||
Parser parser("(x: 10, y: 20");
|
||||
parser.parse();
|
||||
},
|
||||
Base::ParserError
|
||||
);
|
||||
}
|
||||
|
||||
// Test that grouped expressions still work (backward compatibility)
|
||||
TEST_F(ParserTest, GroupedExpressionStillWorks)
|
||||
{
|
||||
{
|
||||
Parser parser("(10 + 5) * 2");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(std::holds_alternative<Numeric>(result));
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(result).value, 30.0);
|
||||
}
|
||||
|
||||
{
|
||||
Parser parser("(42)");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(std::holds_alternative<Numeric>(result));
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(result).value, 42.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Test nested parentheses
|
||||
TEST_F(ParserTest, ParseNestedParentheses)
|
||||
{
|
||||
@@ -627,3 +892,588 @@ TEST_F(ParserTest, ParseNestedParentheses)
|
||||
EXPECT_EQ(length.unit, "");
|
||||
}
|
||||
}
|
||||
|
||||
// Test member access on tuple parameters (named)
|
||||
TEST_F(ParserTest, MemberAccessNamed)
|
||||
{
|
||||
{
|
||||
Parser parser("@TestTupleParam.left");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(std::holds_alternative<Numeric>(result));
|
||||
|
||||
auto length = std::get<Numeric>(result);
|
||||
EXPECT_DOUBLE_EQ(length.value, 1.0);
|
||||
EXPECT_EQ(length.unit, "px");
|
||||
}
|
||||
|
||||
{
|
||||
Parser parser("@TestTupleParam.right");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(std::holds_alternative<Numeric>(result));
|
||||
|
||||
auto length = std::get<Numeric>(result);
|
||||
EXPECT_DOUBLE_EQ(length.value, 2.0);
|
||||
EXPECT_EQ(length.unit, "px");
|
||||
}
|
||||
}
|
||||
|
||||
// Test member access on tuple parameters (indexed)
|
||||
TEST_F(ParserTest, MemberAccessIndexed)
|
||||
{
|
||||
{
|
||||
Parser parser("@TestIndexedTuple.0");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(std::holds_alternative<Numeric>(result));
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(result).value, 10.0);
|
||||
}
|
||||
|
||||
{
|
||||
Parser parser("@TestIndexedTuple.2");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(std::holds_alternative<Numeric>(result));
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(result).value, 30.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Test member access on inline tuples
|
||||
TEST_F(ParserTest, MemberAccessInlineTuple)
|
||||
{
|
||||
{
|
||||
Parser parser("(x: 10, y: 20).x");
|
||||
auto expr = parser.parse();
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(std::holds_alternative<Numeric>(result));
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(result).value, 10.0);
|
||||
}
|
||||
|
||||
{
|
||||
Parser parser("(10, 20, 30).1");
|
||||
auto expr = parser.parse();
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(std::holds_alternative<Numeric>(result));
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(result).value, 20.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Test member access in arithmetic expressions
|
||||
TEST_F(ParserTest, MemberAccessInArithmetic)
|
||||
{
|
||||
Parser parser("@TestTupleParam.left + @TestTupleParam.right");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(std::holds_alternative<Numeric>(result));
|
||||
|
||||
auto length = std::get<Numeric>(result);
|
||||
EXPECT_DOUBLE_EQ(length.value, 3.0);
|
||||
EXPECT_EQ(length.unit, "px");
|
||||
}
|
||||
|
||||
// Test chained member access on nested tuples
|
||||
TEST_F(ParserTest, MemberAccessChained)
|
||||
{
|
||||
Parser parser("@TestNestedTuple.0.x");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(std::holds_alternative<Numeric>(result));
|
||||
EXPECT_DOUBLE_EQ(std::get<Numeric>(result).value, 1.0);
|
||||
}
|
||||
|
||||
// Test member access error cases
|
||||
TEST_F(ParserTest, MemberAccessErrors)
|
||||
{
|
||||
// Named member not found
|
||||
EXPECT_THROW(
|
||||
{
|
||||
Parser parser("@TestTupleParam.nonexistent");
|
||||
auto expr = parser.parse();
|
||||
expr->evaluate({&manager, {}});
|
||||
},
|
||||
Base::ExpressionError
|
||||
);
|
||||
|
||||
// Index out of bounds
|
||||
EXPECT_THROW(
|
||||
{
|
||||
Parser parser("@TestIndexedTuple.5");
|
||||
auto expr = parser.parse();
|
||||
expr->evaluate({&manager, {}});
|
||||
},
|
||||
Base::RuntimeError
|
||||
);
|
||||
|
||||
// Member access on non-tuple
|
||||
EXPECT_THROW(
|
||||
{
|
||||
Parser parser("@TestNumber.foo");
|
||||
auto expr = parser.parse();
|
||||
expr->evaluate({&manager, {}});
|
||||
},
|
||||
Base::ExpressionError
|
||||
);
|
||||
}
|
||||
|
||||
// Tuple arithmetic tests
|
||||
|
||||
TEST_F(ParserTest, TupleElementWiseAdd)
|
||||
{
|
||||
Parser parser("(1px, 2px) + (3px, 4px)");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 2);
|
||||
EXPECT_DOUBLE_EQ(tuple.at(0).get<Numeric>().value, 4.0);
|
||||
EXPECT_EQ(tuple.at(0).get<Numeric>().unit, "px");
|
||||
EXPECT_DOUBLE_EQ(tuple.at(1).get<Numeric>().value, 6.0);
|
||||
EXPECT_EQ(tuple.at(1).get<Numeric>().unit, "px");
|
||||
}
|
||||
|
||||
TEST_F(ParserTest, TupleElementWiseSubtract)
|
||||
{
|
||||
Parser parser("(10, 20) - (3, 7)");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 2);
|
||||
EXPECT_DOUBLE_EQ(tuple.at(0).get<Numeric>().value, 7.0);
|
||||
EXPECT_DOUBLE_EQ(tuple.at(1).get<Numeric>().value, 13.0);
|
||||
}
|
||||
|
||||
TEST_F(ParserTest, TupleScalarMultiplyTupleFirst)
|
||||
{
|
||||
Parser parser("(1px, 2px, 3px) * 2");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 3);
|
||||
EXPECT_DOUBLE_EQ(tuple.at(0).get<Numeric>().value, 2.0);
|
||||
EXPECT_EQ(tuple.at(0).get<Numeric>().unit, "px");
|
||||
EXPECT_DOUBLE_EQ(tuple.at(1).get<Numeric>().value, 4.0);
|
||||
EXPECT_DOUBLE_EQ(tuple.at(2).get<Numeric>().value, 6.0);
|
||||
}
|
||||
|
||||
TEST_F(ParserTest, TupleScalarMultiplyScalarFirst)
|
||||
{
|
||||
Parser parser("2 * (1px, 2px, 3px)");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 3);
|
||||
EXPECT_DOUBLE_EQ(tuple.at(0).get<Numeric>().value, 2.0);
|
||||
EXPECT_EQ(tuple.at(0).get<Numeric>().unit, "px");
|
||||
EXPECT_DOUBLE_EQ(tuple.at(1).get<Numeric>().value, 4.0);
|
||||
EXPECT_DOUBLE_EQ(tuple.at(2).get<Numeric>().value, 6.0);
|
||||
}
|
||||
|
||||
TEST_F(ParserTest, TupleScalarDivide)
|
||||
{
|
||||
Parser parser("(10, 20) / 2");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 2);
|
||||
EXPECT_DOUBLE_EQ(tuple.at(0).get<Numeric>().value, 5.0);
|
||||
EXPECT_DOUBLE_EQ(tuple.at(1).get<Numeric>().value, 10.0);
|
||||
}
|
||||
|
||||
TEST_F(ParserTest, TupleUnaryNegate)
|
||||
{
|
||||
Parser parser("-(1px, 2px)");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 2);
|
||||
EXPECT_DOUBLE_EQ(tuple.at(0).get<Numeric>().value, -1.0);
|
||||
EXPECT_EQ(tuple.at(0).get<Numeric>().unit, "px");
|
||||
EXPECT_DOUBLE_EQ(tuple.at(1).get<Numeric>().value, -2.0);
|
||||
EXPECT_EQ(tuple.at(1).get<Numeric>().unit, "px");
|
||||
}
|
||||
|
||||
TEST_F(ParserTest, TupleAddPreservesNames)
|
||||
{
|
||||
Parser parser("(x: 1, y: 2) + (x: 3, y: 4)");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 2);
|
||||
|
||||
auto* x = tuple.find("x");
|
||||
ASSERT_NE(x, nullptr);
|
||||
EXPECT_DOUBLE_EQ(x->get<Numeric>().value, 4.0);
|
||||
|
||||
auto* y = tuple.find("y");
|
||||
ASSERT_NE(y, nullptr);
|
||||
EXPECT_DOUBLE_EQ(y->get<Numeric>().value, 6.0);
|
||||
}
|
||||
|
||||
TEST_F(ParserTest, TupleAddMatchesByName)
|
||||
{
|
||||
Parser parser("(x: 10, y: 20) + (y: 30, x: 5)");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 2);
|
||||
|
||||
auto* x = tuple.find("x");
|
||||
ASSERT_NE(x, nullptr);
|
||||
EXPECT_DOUBLE_EQ(x->get<Numeric>().value, 15.0);
|
||||
|
||||
auto* y = tuple.find("y");
|
||||
ASSERT_NE(y, nullptr);
|
||||
EXPECT_DOUBLE_EQ(y->get<Numeric>().value, 50.0);
|
||||
}
|
||||
|
||||
TEST_F(ParserTest, TupleParamArithmetic)
|
||||
{
|
||||
Parser parser("@TestTupleParam + @TestTupleParam");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 4);
|
||||
EXPECT_DOUBLE_EQ(tuple.at(0).get<Numeric>().value, 2.0);
|
||||
EXPECT_DOUBLE_EQ(tuple.at(1).get<Numeric>().value, 4.0);
|
||||
EXPECT_DOUBLE_EQ(tuple.at(2).get<Numeric>().value, 6.0);
|
||||
EXPECT_DOUBLE_EQ(tuple.at(3).get<Numeric>().value, 8.0);
|
||||
}
|
||||
|
||||
TEST_F(ParserTest, TupleUnionMixedNamedUnnamed)
|
||||
{
|
||||
// (x: 5, 10) + (y: 10, 5, 20) → (x: 5, y: 10, 15, 20)
|
||||
Parser parser("(x: 5, 10) + (y: 10, 5, 20)");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 4);
|
||||
|
||||
auto* x = tuple.find("x");
|
||||
ASSERT_NE(x, nullptr);
|
||||
EXPECT_DOUBLE_EQ(x->get<Numeric>().value, 5.0);
|
||||
|
||||
auto* y = tuple.find("y");
|
||||
ASSERT_NE(y, nullptr);
|
||||
EXPECT_DOUBLE_EQ(y->get<Numeric>().value, 10.0);
|
||||
|
||||
EXPECT_DOUBLE_EQ(tuple.at(2).get<Numeric>().value, 15.0);
|
||||
EXPECT_DOUBLE_EQ(tuple.at(3).get<Numeric>().value, 20.0);
|
||||
}
|
||||
|
||||
TEST_F(ParserTest, TupleUnionNamedDifferentSizes)
|
||||
{
|
||||
// (x: 1, y: 2) + (x: 3, y: 4, z: 5) → (x: 4, y: 6, z: 5)
|
||||
Parser parser("(x: 1, y: 2) + (x: 3, y: 4, z: 5)");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 3);
|
||||
|
||||
EXPECT_DOUBLE_EQ(tuple.find("x")->get<Numeric>().value, 4.0);
|
||||
EXPECT_DOUBLE_EQ(tuple.find("y")->get<Numeric>().value, 6.0);
|
||||
EXPECT_DOUBLE_EQ(tuple.find("z")->get<Numeric>().value, 5.0);
|
||||
}
|
||||
|
||||
TEST_F(ParserTest, TupleUnionUnnamedDifferentSizes)
|
||||
{
|
||||
// (1, 2) + (3, 4, 5) → (4, 6, 5)
|
||||
Parser parser("(1, 2) + (3, 4, 5)");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
|
||||
const auto& tuple = result.get<Tuple>();
|
||||
EXPECT_EQ(tuple.size(), 3);
|
||||
|
||||
EXPECT_DOUBLE_EQ(tuple.at(0).get<Numeric>().value, 4.0);
|
||||
EXPECT_DOUBLE_EQ(tuple.at(1).get<Numeric>().value, 6.0);
|
||||
EXPECT_DOUBLE_EQ(tuple.at(2).get<Numeric>().value, 5.0);
|
||||
}
|
||||
|
||||
TEST_F(ParserTest, TupleAddNumericError)
|
||||
{
|
||||
EXPECT_THROW(
|
||||
{
|
||||
Parser parser("(1, 2) + 5");
|
||||
auto expr = parser.parse();
|
||||
expr->evaluate({&manager, {}});
|
||||
},
|
||||
Base::ExpressionError
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(ParserTest, TupleNestedScalarMultiply)
|
||||
{
|
||||
Parser parser("((1, 2), (3, 4)) * 2");
|
||||
auto expr = parser.parse();
|
||||
|
||||
auto result = expr->evaluate({&manager, {}});
|
||||
EXPECT_TRUE(result.holds<Tuple>());
|
||||
|
||||
const auto& outer = result.get<Tuple>();
|
||||
EXPECT_EQ(outer.size(), 2);
|
||||
|
||||
EXPECT_TRUE(outer.at(0).holds<Tuple>());
|
||||
const auto& inner1 = outer.at(0).get<Tuple>();
|
||||
EXPECT_DOUBLE_EQ(inner1.at(0).get<Numeric>().value, 2.0);
|
||||
EXPECT_DOUBLE_EQ(inner1.at(1).get<Numeric>().value, 4.0);
|
||||
|
||||
EXPECT_TRUE(outer.at(1).holds<Tuple>());
|
||||
const auto& inner2 = outer.at(1).get<Tuple>();
|
||||
EXPECT_DOUBLE_EQ(inner2.at(0).get<Numeric>().value, 6.0);
|
||||
EXPECT_DOUBLE_EQ(inner2.at(1).get<Numeric>().value, 8.0);
|
||||
}
|
||||
|
||||
// ArgumentParser tests
|
||||
|
||||
class ArgumentParserTest: public ::testing::Test
|
||||
{
|
||||
struct NameValuePair
|
||||
{
|
||||
std::optional<std::string> name;
|
||||
Value value;
|
||||
};
|
||||
|
||||
protected:
|
||||
static Tuple makeTuple(std::vector<NameValuePair> elements)
|
||||
{
|
||||
Tuple tuple;
|
||||
for (auto& [name, value] : elements) {
|
||||
tuple.elements.emplace_back(name, std::make_shared<const Value>(std::move(value)));
|
||||
}
|
||||
return tuple;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ArgumentParserTest, AllPositional)
|
||||
{
|
||||
auto args = makeTuple({
|
||||
{.name = std::nullopt, .value = Numeric {1, ""}},
|
||||
{.name = std::nullopt, .value = Numeric {2, ""}},
|
||||
});
|
||||
auto resolved = ArgumentParser {{"x"}, {"y"}}.resolve(args);
|
||||
|
||||
ASSERT_NE(resolved.find("x"), nullptr);
|
||||
ASSERT_NE(resolved.find("y"), nullptr);
|
||||
EXPECT_DOUBLE_EQ(resolved.find("x")->get<Numeric>().value, 1.0);
|
||||
EXPECT_DOUBLE_EQ(resolved.find("y")->get<Numeric>().value, 2.0);
|
||||
}
|
||||
|
||||
TEST_F(ArgumentParserTest, AllNamed)
|
||||
{
|
||||
auto args = makeTuple({
|
||||
{.name = std::string("x"), .value = Numeric {10, ""}},
|
||||
{.name = std::string("y"), .value = Numeric {20, ""}},
|
||||
});
|
||||
auto resolved = ArgumentParser {{"x"}, {"y"}}.resolve(args);
|
||||
|
||||
EXPECT_DOUBLE_EQ(resolved.find("x")->get<Numeric>().value, 10.0);
|
||||
EXPECT_DOUBLE_EQ(resolved.find("y")->get<Numeric>().value, 20.0);
|
||||
}
|
||||
|
||||
TEST_F(ArgumentParserTest, AllNamedReversedOrder)
|
||||
{
|
||||
auto args = makeTuple({
|
||||
{.name = std::string("y"), .value = Numeric {20, ""}},
|
||||
{.name = std::string("x"), .value = Numeric {10, ""}},
|
||||
});
|
||||
auto resolved = ArgumentParser {{"x"}, {"y"}}.resolve(args);
|
||||
|
||||
EXPECT_DOUBLE_EQ(resolved.find("x")->get<Numeric>().value, 10.0);
|
||||
EXPECT_DOUBLE_EQ(resolved.find("y")->get<Numeric>().value, 20.0);
|
||||
}
|
||||
|
||||
TEST_F(ArgumentParserTest, MixedPositionalThenNamed)
|
||||
{
|
||||
// f(1, y: 2) with signature (x, y)
|
||||
auto args = makeTuple({
|
||||
{.name = std::nullopt, .value = Numeric {1, ""}},
|
||||
{.name = std::string("y"), .value = Numeric {2, ""}},
|
||||
});
|
||||
auto resolved = ArgumentParser {{"x"}, {"y"}}.resolve(args);
|
||||
|
||||
EXPECT_DOUBLE_EQ(resolved.find("x")->get<Numeric>().value, 1.0);
|
||||
EXPECT_DOUBLE_EQ(resolved.find("y")->get<Numeric>().value, 2.0);
|
||||
}
|
||||
|
||||
TEST_F(ArgumentParserTest, NamedThenPositionalFillsRemainingSlot)
|
||||
{
|
||||
// f(y: 2, 1) with signature (x, y) — positional 1 fills unclaimed x
|
||||
auto args = makeTuple({
|
||||
{.name = std::string("y"), .value = Numeric {2, ""}},
|
||||
{.name = std::nullopt, .value = Numeric {1, ""}},
|
||||
});
|
||||
auto resolved = ArgumentParser {{"x"}, {"y"}}.resolve(args);
|
||||
|
||||
EXPECT_DOUBLE_EQ(resolved.find("x")->get<Numeric>().value, 1.0);
|
||||
EXPECT_DOUBLE_EQ(resolved.find("y")->get<Numeric>().value, 2.0);
|
||||
}
|
||||
|
||||
TEST_F(ArgumentParserTest, DefaultValueUsedWhenMissing)
|
||||
{
|
||||
auto args = makeTuple({
|
||||
{.name = std::nullopt, .value = Numeric {1, ""}},
|
||||
});
|
||||
auto resolved = ArgumentParser {{"x"}, {"y", Numeric {99, ""}}}.resolve(args);
|
||||
|
||||
EXPECT_DOUBLE_EQ(resolved.find("x")->get<Numeric>().value, 1.0);
|
||||
EXPECT_DOUBLE_EQ(resolved.find("y")->get<Numeric>().value, 99.0);
|
||||
}
|
||||
|
||||
TEST_F(ArgumentParserTest, DefaultValueOverriddenByPositional)
|
||||
{
|
||||
auto args = makeTuple({
|
||||
{.name = std::nullopt, .value = Numeric {1, ""}},
|
||||
{.name = std::nullopt, .value = Numeric {2, ""}},
|
||||
});
|
||||
auto resolved = ArgumentParser {{"x"}, {"y", Numeric {99, ""}}}.resolve(args);
|
||||
|
||||
EXPECT_DOUBLE_EQ(resolved.find("y")->get<Numeric>().value, 2.0);
|
||||
}
|
||||
|
||||
TEST_F(ArgumentParserTest, DefaultValueOverriddenByName)
|
||||
{
|
||||
auto args = makeTuple({
|
||||
{.name = std::nullopt, .value = Numeric {1, ""}},
|
||||
{.name = std::string("y"), .value = Numeric {2, ""}},
|
||||
});
|
||||
auto resolved = ArgumentParser {{"x"}, {"y", Numeric {99, ""}}}.resolve(args);
|
||||
|
||||
EXPECT_DOUBLE_EQ(resolved.find("y")->get<Numeric>().value, 2.0);
|
||||
}
|
||||
|
||||
TEST_F(ArgumentParserTest, ResolvedTupleHasCorrectOrder)
|
||||
{
|
||||
auto args = makeTuple({
|
||||
{.name = std::string("y"), .value = Numeric {2, ""}},
|
||||
{.name = std::nullopt, .value = Numeric {1, ""}},
|
||||
});
|
||||
auto resolved = ArgumentParser {{"x"}, {"y"}}.resolve(args);
|
||||
|
||||
// at(0) is x, at(1) is y — matches declaration order
|
||||
EXPECT_DOUBLE_EQ(resolved.at(0).get<Numeric>().value, 1.0);
|
||||
EXPECT_DOUBLE_EQ(resolved.at(1).get<Numeric>().value, 2.0);
|
||||
}
|
||||
|
||||
TEST_F(ArgumentParserTest, MixedTypes)
|
||||
{
|
||||
auto args = makeTuple({
|
||||
{.name = std::nullopt, .value = Base::Color(1.0, 0.0, 0.0)},
|
||||
{.name = std::nullopt, .value = Numeric {20, ""}},
|
||||
});
|
||||
auto resolved = ArgumentParser {{"color"}, {"amount"}}.resolve(args);
|
||||
|
||||
EXPECT_TRUE(resolved.find("color")->holds<Base::Color>());
|
||||
EXPECT_TRUE(resolved.find("amount")->holds<Numeric>());
|
||||
}
|
||||
|
||||
TEST_F(ArgumentParserTest, ErrorOnUnknownName)
|
||||
{
|
||||
auto args = makeTuple({
|
||||
{.name = std::string("unknown"), .value = Numeric {1, ""}},
|
||||
});
|
||||
ArgumentParser parser {{"x"}, {"y"}};
|
||||
EXPECT_THROW(parser.resolve(args), Base::ExpressionError);
|
||||
}
|
||||
|
||||
TEST_F(ArgumentParserTest, ErrorOnDuplicateName)
|
||||
{
|
||||
auto args = makeTuple({
|
||||
{.name = std::string("x"), .value = Numeric {1, ""}},
|
||||
{.name = std::string("x"), .value = Numeric {2, ""}},
|
||||
});
|
||||
ArgumentParser parser {{"x"}, {"y"}};
|
||||
EXPECT_THROW(parser.resolve(args), Base::ExpressionError);
|
||||
}
|
||||
|
||||
TEST_F(ArgumentParserTest, ErrorOnMissingRequired)
|
||||
{
|
||||
auto args = makeTuple({
|
||||
{.name = std::nullopt, .value = Numeric {1, ""}},
|
||||
});
|
||||
ArgumentParser parser {{"x"}, {"y"}};
|
||||
EXPECT_THROW(parser.resolve(args), Base::ExpressionError);
|
||||
}
|
||||
|
||||
TEST_F(ArgumentParserTest, ErrorOnExcessPositional)
|
||||
{
|
||||
auto args = makeTuple({
|
||||
{.name = std::nullopt, .value = Numeric {1, ""}},
|
||||
{.name = std::nullopt, .value = Numeric {2, ""}},
|
||||
{.name = std::nullopt, .value = Numeric {3, ""}},
|
||||
});
|
||||
ArgumentParser parser {{"x"}, {"y"}};
|
||||
EXPECT_THROW(parser.resolve(args), Base::ExpressionError);
|
||||
}
|
||||
|
||||
TEST_F(ArgumentParserTest, TypedGetSuccess)
|
||||
{
|
||||
auto args = makeTuple({
|
||||
{.name = std::nullopt, .value = Base::Color(1.0, 0.0, 0.0)},
|
||||
{.name = std::nullopt, .value = Numeric {20, ""}},
|
||||
});
|
||||
auto resolved = ArgumentParser {{"color"}, {"amount"}}.resolve(args);
|
||||
|
||||
EXPECT_NO_THROW(resolved.get<Base::Color>("color"));
|
||||
EXPECT_NO_THROW(resolved.get<Numeric>("amount"));
|
||||
EXPECT_DOUBLE_EQ(resolved.get<Numeric>("amount").value, 20.0);
|
||||
}
|
||||
|
||||
TEST_F(ArgumentParserTest, TypedGetWrongType)
|
||||
{
|
||||
auto args = makeTuple({
|
||||
{.name = std::nullopt, .value = Numeric {10, "px"}},
|
||||
{.name = std::nullopt, .value = Numeric {20, ""}},
|
||||
});
|
||||
auto resolved = ArgumentParser {{"color"}, {"amount"}}.resolve(args);
|
||||
|
||||
EXPECT_THROW(resolved.get<Base::Color>("color"), Base::ExpressionError);
|
||||
}
|
||||
|
||||
TEST_F(ArgumentParserTest, TypedGetMissingName)
|
||||
{
|
||||
auto args = makeTuple({{.name = std::nullopt, .value = Numeric {10, ""}}});
|
||||
auto resolved = ArgumentParser {{"x"}}.resolve(args);
|
||||
|
||||
EXPECT_THROW(resolved.get<Numeric>("nonexistent"), Base::ExpressionError);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user