css: Implement math functions

Implement the functions described in the "Mathematical
Expressions" section of the "CSS Values and Units Module
Level 4" spec, https://www.w3.org/TR/css-values-4/.

Beyond calc(), which we already had, this includes
min(), max(), clamp(),
round(), rem(), mod(),
sin(), cos(), tan(), asin(), acos(), atan(), atan2(),
pow(), sqrt(), hypot(), log(), exp(),
abs(), sign(),
e, pi, infinity and NaN.

Some tests included.
This commit is contained in:
Matthias Clasen 2024-05-11 17:35:54 -04:00
parent af0c277bba
commit 4e6759a126
10 changed files with 1878 additions and 222 deletions

View File

@ -232,3 +232,307 @@ gtk_css_calc_value_parse (GtkCssParser *parser,
return data.value;
}
typedef struct
{
GtkCssNumberParseFlags flags;
GPtrArray *values;
} ParseArgnData;
static guint
gtk_css_argn_value_parse_arg (GtkCssParser *parser,
guint arg,
gpointer data_)
{
ParseArgnData *data = data_;
GtkCssValue *value;
value = gtk_css_calc_value_parse_sum (parser, data->flags);
if (value == NULL)
return 0;
g_ptr_array_add (data->values, value);
return 1;
}
typedef struct
{
GtkCssNumberParseFlags flags;
GtkCssValue *values[3];
} ParseClampData;
static guint
gtk_css_clamp_value_parse_arg (GtkCssParser *parser,
guint arg,
gpointer data_)
{
ParseClampData *data = data_;
if ((arg == 0 || arg == 2))
{
if (gtk_css_parser_try_ident (parser, "none"))
{
data->values[arg] = NULL;
return 1;
}
}
data->values[arg] = gtk_css_calc_value_parse_sum (parser, data->flags);
if (data->values[arg] == NULL)
return 0;
return 1;
}
GtkCssValue *
gtk_css_clamp_value_parse (GtkCssParser *parser,
GtkCssNumberParseFlags flags,
guint type)
{
ParseClampData data;
GtkCssValue *result = NULL;
if (!gtk_css_parser_has_function (parser, "clamp"))
{
gtk_css_parser_error_syntax (parser, "Expected 'clamp('");
return NULL;
}
/* This can only be handled at compute time, we allow '-' after all */
data.flags = flags & ~GTK_CSS_POSITIVE_ONLY;
data.values[0] = NULL;
data.values[1] = NULL;
data.values[2] = NULL;
if (gtk_css_parser_consume_function (parser, 3, 3, gtk_css_clamp_value_parse_arg, &data))
{
GtkCssDimension dim = gtk_css_number_value_get_dimension (data.values[1]);
if ((data.values[0] && gtk_css_number_value_get_dimension (data.values[0]) != dim) ||
(data.values[2] && gtk_css_number_value_get_dimension (data.values[2]) != dim))
gtk_css_parser_error_syntax (parser, "Inconsistent types in 'clamp('");
else
result = gtk_css_math_value_new (type, 0, data.values, 3);
}
if (result == NULL)
{
g_clear_pointer (&data.values[0], gtk_css_value_unref);
g_clear_pointer (&data.values[1], gtk_css_value_unref);
g_clear_pointer (&data.values[2], gtk_css_value_unref);
}
return result;
}
typedef struct {
GtkCssNumberParseFlags flags;
guint mode;
gboolean has_mode;
GtkCssValue *values[2];
} ParseRoundData;
static guint
gtk_css_round_value_parse_arg (GtkCssParser *parser,
guint arg,
gpointer data_)
{
ParseRoundData *data = data_;
if (arg == 0)
{
const char *modes[] = { "nearest", "up", "down", "to-zero" };
for (guint i = 0; i < G_N_ELEMENTS (modes); i++)
{
if (gtk_css_parser_try_ident (parser, modes[i]))
{
data->mode = i;
data->has_mode = TRUE;
return 1;
}
}
data->values[0] = gtk_css_calc_value_parse_sum (parser, data->flags);
if (data->values[0] == NULL)
return 0;
}
else if (arg == 1)
{
GtkCssValue *value = gtk_css_calc_value_parse_sum (parser, data->flags);
if (value == NULL)
return 0;
if (data->has_mode)
data->values[0] = value;
else
data->values[1] = value;
}
else
{
if (!data->has_mode)
{
gtk_css_parser_error_syntax (parser, "Too many argument for 'round'");
return 0;
}
data->values[1] = gtk_css_calc_value_parse_sum (parser, data->flags);
if (data->values[1] == NULL)
return 0;
}
return 1;
}
GtkCssValue *
gtk_css_round_value_parse (GtkCssParser *parser,
GtkCssNumberParseFlags flags,
guint type)
{
ParseRoundData data;
GtkCssValue *result = NULL;
if (!gtk_css_parser_has_function (parser, "round"))
{
gtk_css_parser_error_syntax (parser, "Expected 'round('");
return NULL;
}
data.flags = flags & ~GTK_CSS_POSITIVE_ONLY;
data.mode = ROUND_NEAREST;
data.has_mode = FALSE;
data.values[0] = NULL;
data.values[1] = NULL;
if (gtk_css_parser_consume_function (parser, 1, 3, gtk_css_round_value_parse_arg, &data) &&
data.values[0] != NULL)
{
if (data.values[1] != NULL &&
gtk_css_number_value_get_dimension (data.values[0]) !=
gtk_css_number_value_get_dimension (data.values[1]))
gtk_css_parser_error_syntax (parser, "Inconsistent types in 'round('");
else if (data.values[1] == NULL &&
gtk_css_number_value_get_dimension (data.values[0]) != GTK_CSS_DIMENSION_NUMBER)
gtk_css_parser_error_syntax (parser, "Can't omit second argument to 'round(' here");
else
result = gtk_css_math_value_new (type, data.mode, data.values, data.values[1] != NULL ? 2 : 1);
}
if (result == NULL)
{
g_clear_pointer (&data.values[0], gtk_css_value_unref);
g_clear_pointer (&data.values[1], gtk_css_value_unref);
}
return result;
}
typedef struct {
GtkCssNumberParseFlags flags;
GtkCssValue *values[2];
} ParseArg2Data;
static guint
gtk_css_arg2_value_parse_arg (GtkCssParser *parser,
guint arg,
gpointer data_)
{
ParseArg2Data *data = data_;
data->values[arg] = gtk_css_calc_value_parse_sum (parser, data->flags);
if (data->values[arg] == NULL)
return 0;
return 1;
}
GtkCssValue *
gtk_css_arg2_value_parse (GtkCssParser *parser,
GtkCssNumberParseFlags flags,
guint min_args,
guint max_args,
const char *function,
guint type)
{
ParseArg2Data data;
GtkCssValue *result = NULL;
g_assert (1 <= min_args && min_args <= max_args && max_args <= 2);
if (!gtk_css_parser_has_function (parser, function))
{
gtk_css_parser_error_syntax (parser, "Expected '%s('", function);
return NULL;
}
data.flags = flags & ~GTK_CSS_POSITIVE_ONLY;
data.values[0] = NULL;
data.values[1] = NULL;
if (gtk_css_parser_consume_function (parser, min_args, max_args, gtk_css_arg2_value_parse_arg, &data))
{
if (data.values[1] != NULL &&
gtk_css_number_value_get_dimension (data.values[0]) !=
gtk_css_number_value_get_dimension (data.values[1]))
gtk_css_parser_error_syntax (parser, "Inconsistent types in '%s('", function);
else
result = gtk_css_math_value_new (type, 0, data.values, data.values[1] != NULL ? 2 : 1);
}
if (result == NULL)
{
g_clear_pointer (&data.values[0], gtk_css_value_unref);
g_clear_pointer (&data.values[1], gtk_css_value_unref);
}
return result;
}
GtkCssValue *
gtk_css_argn_value_parse (GtkCssParser *parser,
GtkCssNumberParseFlags flags,
const char *function,
guint type)
{
ParseArgnData data;
GtkCssValue *result = NULL;
if (!gtk_css_parser_has_function (parser, function))
{
gtk_css_parser_error_syntax (parser, "Expected '%s('", function);
return NULL;
}
/* This can only be handled at compute time, we allow '-' after all */
data.flags = flags & ~GTK_CSS_POSITIVE_ONLY;
data.values = g_ptr_array_new ();
if (gtk_css_parser_consume_function (parser, 1, G_MAXUINT, gtk_css_argn_value_parse_arg, &data))
{
GtkCssValue *val = (GtkCssValue *) g_ptr_array_index (data.values, 0);
GtkCssDimension dim = gtk_css_number_value_get_dimension (val);
guint i;
for (i = 1; i < data.values->len; i++)
{
val = (GtkCssValue *) g_ptr_array_index (data.values, i);
if (gtk_css_number_value_get_dimension (val) != dim)
break;
}
if (i < data.values->len)
gtk_css_parser_error_syntax (parser, "Inconsistent types in '%s('", function);
else
result = gtk_css_math_value_new (type, 0, (GtkCssValue **)data.values->pdata, data.values->len);
}
if (result == NULL)
{
for (guint i = 0; i < data.values->len; i++)
gtk_css_value_unref ((GtkCssValue *)g_ptr_array_index (data.values, i));
}
g_ptr_array_unref (data.values);
return result;
}

View File

@ -23,6 +23,21 @@ G_BEGIN_DECLS
GtkCssValue * gtk_css_calc_value_parse (GtkCssParser *parser,
GtkCssNumberParseFlags flags);
GtkCssValue * gtk_css_clamp_value_parse (GtkCssParser *parser,
GtkCssNumberParseFlags flags,
guint type);
GtkCssValue * gtk_css_round_value_parse (GtkCssParser *parser,
GtkCssNumberParseFlags flags,
guint type);
GtkCssValue * gtk_css_arg2_value_parse (GtkCssParser *parser,
GtkCssNumberParseFlags flags,
guint min_args,
guint max_args,
const char *function,
guint type);
GtkCssValue * gtk_css_argn_value_parse (GtkCssParser *parser,
GtkCssNumberParseFlags flags,
const char *function,
guint type);
G_END_DECLS

File diff suppressed because it is too large Load Diff

View File

@ -36,6 +36,8 @@ typedef enum /*< skip >*/ {
GTK_CSS_PARSE_TIME = (1 << 5)
} GtkCssNumberParseFlags;
#define GTK_CSS_PARSE_DIMENSION (GTK_CSS_PARSE_LENGTH|GTK_CSS_PARSE_ANGLE|GTK_CSS_PARSE_TIME)
GtkCssValue * gtk_css_dimension_value_new (double value,
GtkCssUnit unit);
@ -58,5 +60,17 @@ double _gtk_css_number_value_get (const GtkCssValue *num
gboolean gtk_css_dimension_value_is_zero (const GtkCssValue *value) G_GNUC_PURE;
enum {
ROUND_NEAREST,
ROUND_UP,
ROUND_DOWN,
ROUND_TO_ZERO,
};
GtkCssValue * gtk_css_math_value_new (guint type,
guint mode,
GtkCssValue **values,
guint n_values);
G_END_DECLS

View File

@ -3,7 +3,7 @@ a {
}
b {
transition-duration: calc(-200ms + 1s);
transition-duration: 0.80000000000000004s;
}
c {

View File

@ -0,0 +1,88 @@
a {
margin-left: min(3px, 1em, 20cm);
margin-right: min(3px, 5px, 30px);
margin-top: calc(1px * min(3, 5, 10));
}
b {
padding-left: max(3px, 4em, 2px, 20cm);
padding-top: calc(1cm * max(3, 5, 7));
transition-duration: max(1s, 100ms);
}
c {
background-size: clamp(none, calc(pi * 1pt), 10cm);
border-left-width: clamp(2px, calc(e * 1px), none);
margin-left: clamp(2px, 1em, 10cm);
}
d {
background-size: round(nearest, 17.3px, 2.2px);
border-left-width: round(down, 17px, 3ex);
border-right-width: calc(1px * round(17.5));
border-top-width: round(up, 17px, 3px);
}
e {
background-size: rem(17.3px, 2.2px);
}
f {
border-top-left-radius: mod(-17.3px, 2.2px);
}
g {
border-bottom-right-radius: calc(1px * sin(20));
}
h {
margin-left: calc(1px * cos(20));
}
i {
margin-right: calc(1px * tan(20));
}
j {
filter: hue-rotate(asin(20));
}
k {
filter: hue-rotate(acos(20));
}
l {
filter: hue-rotate(atan(20));
}
m {
filter: hue-rotate(atan2(20px, 30px));
}
n {
margin-top: calc(1px * pow(2, 3.5));
}
o {
margin-bottom: calc(1px * sqrt(2));
}
p {
padding-left: calc(1px * hypot(2, 3, 4, 5));
}
q {
padding-right: calc(1px * log(8, 3));
}
r {
padding-top: calc(1px * exp(5.4));
}
s {
padding-bottom: calc(1px * abs(sin(e)));
}
t {
border-width: calc(1px * pow(pi, sign(-infinity)));
}

View File

@ -0,0 +1,91 @@
a {
margin-left: min(3px, 1em, 20cm);
margin-right: 3px;
margin-top: 3px;
}
b {
padding-left: max(3px, 4em, 20cm);
padding-top: 7cm;
transition-duration: 1s;
}
c {
background-size: 1.1082840819977162mm;
border-left-width: 2.7182818284590451px;
margin-left: clamp(2px, 1em, 10cm);
}
d {
background-size: 17.600000000000001px;
border-left-width: round(down, 17px, 3ex);
border-right-width: 18px;
border-top-width: 18px;
}
e {
background-size: 1.8999999999999995;
}
f {
border-top-left-radius: 0.30000000000000071;
}
g {
border-bottom-right-radius: 0.91294525072762767px;
}
h {
margin-left: 0.40808206181339196px;
}
i {
margin-right: 2.2371609442247422px;
}
j {
filter: hue-rotate(NaN);
}
k {
filter: hue-rotate(NaN);
}
l {
filter: hue-rotate(87.137594773888253deg);
}
m {
filter: hue-rotate(33.690067525979785deg);
}
n {
margin-top: 11.313708498984761px;
}
o {
margin-bottom: 1.4142135623730951px;
}
p {
padding-left: 7.3484692283495345px;
}
q {
padding-right: 1.8927892607143719px;
}
r {
padding-top: 221.40641620418717px;
}
s {
padding-bottom: 0.41078129050290885px;
}
t {
border-bottom-width: 0.31830988618379069px;
border-left-width: 0.31830988618379069px;
border-right-width: 0.31830988618379069px;
border-top-width: 0.31830988618379069px;
}

View File

@ -0,0 +1,50 @@
a {
margin-left: calc(1px * mod(5, 0));
margin-right: calc(1px * mod(infinity, 2));
margin-top: calc(1px * mod(-1, infinity));
padding-left: mod(18px, 5px);
padding-right: mod(18px, -5px);
padding-top: mod(-18px, 5px);
}
b {
margin-left: calc(1px * rem(5, 0));
margin-right: calc(1px * rem(infinity, 2));
margin-top: calc(1px * rem(-1, infinity));
padding-left: rem(18px, 5px);
padding-right: rem(18px, -5px);
padding-top: rem(-18px, 5px);
}
c {
margin-bottom: round(to-zero, 18px, 5px);
margin-left: round(up, 18px, 5px);
margin-right: round(down, 18px, 5px);
margin-top: round(nearest, 18px, 5px);
}
d {
margin-left: abs(20px);
margin-right: round(1px * cos(abs(-90deg)), 1px);
margin-top: calc(1px * sign(-20deg));
}
e {
margin-bottom: calc(1px * round(18, 0));
margin-left: calc(1px * round(infinity, infinity));
margin-right: calc(1px * round(infinity, 5));
margin-top: calc(1px * round(18, infinity));
padding-bottom: calc(1px * round(nearest, 18, infinity));
padding-left: calc(1px * round(up, 18, infinity));
padding-right: calc(1px * round(down, -18, infinity));
padding-top: calc(1px * round(to-zero, 18, infinity));
}
f {
margin-bottom: mod(18px, 0px);
margin-left: mod(calc(infinity * 1px), 5px);
margin-right: mod(-18px, calc(infinity * 1px));
}

View File

@ -0,0 +1,47 @@
a {
margin-left: NaN;
margin-right: NaN;
margin-top: NaN;
padding-left: 3;
padding-right: 3;
padding-top: 2;
}
b {
margin-left: NaN;
margin-right: NaN;
margin-top: -1px;
padding-left: 3;
padding-right: 3;
padding-top: -3;
}
c {
margin-bottom: 15px;
margin-left: 20px;
margin-right: 15px;
margin-top: 20px;
}
d {
margin-left: 20px;
margin-right: 0;
margin-top: -1px;
}
e {
margin-bottom: NaN;
margin-left: NaN;
margin-right: infinite;
margin-top: 0;
padding-bottom: 0;
padding-left: infinite;
padding-right: -infinite;
padding-top: 0;
}
f {
margin-bottom: NaN;
margin-left: NaN;
margin-right: NaN;
}

View File

@ -414,6 +414,10 @@ test_data = [
'line-height-invalid3.ref.css',
'margin.css',
'margin.ref.css',
'math.css',
'math.ref.css',
'math2.css',
'math2.ref.css',
'min-height.css',
'min-height.ref.css',
'min-width.css',