Open jimmyfrasche opened 4 years ago
Desugarings of if(cond, then, else)
in terms of sign()
:
if(A < B, C, D) = max(sign(B - A), 0) * C + (1 - max(sign(B - A), 0)) * D;
if(A >= B, C, D) = max(sign(B - A), 0) * D + (1 - max(sign(B - A), 0)) * C;
if(A > B, C, D) = max(sign(A - B), 0) * C + (1 - max(sign(A - B), 0)) * D
if(A <= B, C, D) = max(sign(A - B), 0) * D + (1 - max(sign(A - B), 0)) * C
if(A != B, C, D) = abs(sign(B - A)) * C + (1 - abs(sign(B - A))) * D;
if(A == B, C, D) = abs(sign(B - A)) * D + (1 - abs(sign(B - A))) * C;
if(cond1 && cond2, C, D) = cond1 * cond2 * C + (1 - cond1 * cond2) * D;
if(cond1 || cond2, C, D) = sign(cond1 + cond2) * C + (1 - sign(cond1 + cond2)) * D;
if(!cond, C, D) = cond * D + (1 - cond) * C;
(note abs(A) = A * sign(A)
and max(sign(A), 0) = sign(A) * (1 + sign(A)) / 2
).
The problem are comparisons with NaN or infinity.
Perhaps, CSS would be better suited with a generic compare()
function that compared its first two parameters and returned one of the others accordingly, or a set of three slightly more specific functions, equal()
, lesser()
and greater()
:
if(A < B, C, D) = lesser(A, B, C, D) =
if(B > A, C, D) = greater(B, A, C, D) =
if(A >= B, D, C) =
if(B =< A, D, C) =
compare(A, B, C, D, D) =
compare(B, A, D, C, D)
if(A >= B, C, D) =
if(B =< A, C, D) =
if(A < B, D, C) = lesser(A, B, D, C) =
if(B > A, D, C) = greater(B, A, D, C) =
compare(A, B, D, C, C) =
compare(B, A, C, D, C)
if(A > B, C, D) = greater(A, B, C, D) =
if(B < A, C, D) = lesser(B, A, C, D) =
if(A =< B, D, C) =
if(B >= A, D, C) =
compare(A, B, D, C, D) =
compare(B, A, C, D, D)
if(A =< B, C, D) =
if(B >= A, C, D) =
if(A > B, D, C) = greater(A, B, D, C) =
if(B < A, D, C) = lesser(B, A, D, C) =
compare(A, B, C, D, C) =
compare(B, A, D, C, C)
if(A != B, C, D) = compare(A, B, C, D) =
if(B != A, C, D) = compare(B, A, C, D) =
if(A == B, D, C) = equal(A, B, D, C) =
if(B == A, D, C) = equal(A, B, D, C)
if(A == B, C, D) = equal(A, B, C, D) =
if(B == A, C, D) = equal(B, A, C, D) =
if(A != B, D, C) = compare(A, B, D, C) =
if(B != A, D, C) = compare(B, A, D, C)
(The order and fallback of the comparison function parameters could probably be optimized more.)
Considering that there are already tokens for stuff like <
and or
(though not one for ≠ that I could find), I don't see much of a savings by excluding those and the readability isn't great, especially if the third argument means different things depending on the total number of arguments.
With abs/sign, as demonstrated above, you can already do most everything proposed here with css as specced, so this is about making it easier to do those things (and thus harder to accidentally do it the wrong way), making it clearer when you do those things, and possibly adding in a little extra that can't be done yet—or at least polishing the edge cases so they're not as sharp.
Iʼm not particularly fond of compare()
either. I like the set of three functions better.
If only the first parameter was mandatory, the second could default to zero, the third to the first and the fourth to the second.
So if you have X and you want to add Y when A < B && B >= C || D != E
with those three functions one way to write it would be
X + Y * lesser(A, B, 1, 0) * sign(
sign(greater(B, C, 1, 0) + equal(B, C, 1, 0))
+ calc(1 - equal(D, E, 1, 0))
)
With if() it's a bit more straightforward
X + Y * if(A < B and B >= C or D != E)
(I'm assuming that and
has greater precedence than or
.)
If I messed something up but that just proves my point that it's too difficult :smile:
I propose three mathematical functions inspired by Iverson bracket notation — if(p), media(q), supports(s)
If we want boolean functions, I think it may be clearer if we consider a new type
<bool> = true | false
This wouldn't be a numeric value (I guess it could be a CSSKeywordValue, but I don't know much about Typed OM), so things like <bool> + <bool>
or min(<bool>, <bool>)
wouldn't be valid.
A media(q)
or supports(s)
would have type <bool>
.
Operators like <
, !=
, &&
would attempt to add the types of the left and right arguments. If this returns failure, the entire calculation’s type would be failure. Otherwise, the returned type would be <bool>
Then, if(<bool>, then?, else?)
would attempt to add the types of the then
and else
arguments. If this returns failure, the entire calculation’s type would be failure. Otherwise, the sub-expression’s type would be the returned type. If omitted, then
would default to 1
, and else
would default to a numeric value 0 with the same type as then
.
So your example would become
:root {
--dur: if(media(prefers-reduced-motion), 5s);
}
This makes it more clear that we want to set 5s
conditionally, not set some random multiple of 5s
.
Probably, if(<bool>, then, else)
should also accept then
and else
to be <bool>
, and then return <bool>
:
if(if(A > B, C > D, E > F), 10px, 20px);
/* same as */
if((A > B && C > D) || (!(A > B) && E > F), 10px, 20px);
So you couldn't do width: if(some-predicate, 500px, auto)
?
I don't know. Opening the door to that may introduce some problems.
For example, justify-self: stretch
only works if width
computes to auto
. But what if you have something like width: if(1% < 0px, 500px, auto)
? The percentage can't be resolved at computed-value time, so you can't simplify the conditional as either 500px
or auto
.
IMO it would seem unexpected if stretching is allowed with auto
but not with if(1% < 0px, 500px, auto)
, if the condition ends up being false.
So we should probably change css-align to accomodate conditionals that end up resolving to auto
. But the terminology may get difficult. We can't say that the used value is auto
, because the used value is a length. And there is already enough confusion with "behaves as auto".
would that also create problems with using % in a conditional for setting width in something whose parent is intrinsically sized? I'm not sure when all that stuff gets worked out.
I would expect this to be handled by https://drafts.csswg.org/css-sizing/#percentage-sizing. When calculating intrinsic contributions, a cyclic percentage may be treated as zero or the whole expression may be treated as initial
.
Thanks for the link.
Are keywords in general dangerous? Could width: if(p, auto, inherit)
also cause problems?
I'm fine with it being purely numeric but it would be nice if you could write, say, visibility: if(P, visible, hidden);
especially for having things happen based on aria- attributes to make it easier to keep js and css in sync
Note that inherit
is resolved very early in the cascade, when calculating specified values. But the condition may depend on layout information that we don't have at that time. Maybe an inherit
-like feature that resolves later could be added, but it doesn't exist right now.
And yes, keywords seem more problematic, e.g. display: if(1% > 1px, none, block)
. Here it's not just a cyclic percentage during sizing, it directly affects the box tree. It's not as easy as saying that the percentage is treated in some way during intrinsic sizing, and then it's honored or not during actual layout. Because the existence or non-existence of boxes can have wider implications.
Possibly we could add some constraints, like restricting the properties that accept if()
(would be invalid in display
anyways if keywords are not accepted). There have been some ideas in this direction for container queries. But going this way makes if()
more fundamentally different than math functions, and thus less likely to become a reality soon.
So if you have X and you want to add Y when A < B && B >= C || D != E with those three functions one way to write it would be
calc(X + lesser(A, B, greater(C, B, equal(D, E, 0, Y), Y), 0))
Is that an example from a realistic use case or just a strawman?
Actually, another possibility would be some kind of ternary operators inside calc()
:
calc(X + (A < B && B >= C || D != E) ? Y : 0)
calc(X + (A < B and B >= C or D != E) ? Y : 0)
calc(X + if A < B and B >= C or D != E then Y else 0)
calc(X + if A lt B and B ge C or D ne E then Y else 0)
calc(X + (and(A < B, or(B >= C, D != E))) ? Y : 0)
calc(X + Y when(A < B and B >= C or D != E) 0 otherwise)
calc(X + 0 unless A < B and B >= C or D != E then Y)
Some of these are of course very unfamiliar from outside CSS (especially from JS), but they could be appropriate nevertheless.
I don't expect much day-to-day code to have as complicated a condition, no.
Logical conditions would be useful in real code for stuff like if(media(something) or var(--force))
. I expect them to be relatively common uses of if().
I expect they'd get the most use in css frameworks that define a lot of stuff with custom properties that act as knobs.
At some point, I'd hope that we could define reusable custom calc functions directly in css and I imagine libraries of such functions could involve somewhat complicated conditionals. Cf. my comment: https://github.com/w3c/css-houdini-drafts/issues/857#issuecomment-579084136
Does if(if(A > B, C > D, E > F), 10px, 20px)
mean that the logical and comparative operators need to be added to calc expressions and that if()
is essentially a handy way of converting boolean-typed calc expressions into non-bools? Otherwise, it would need to be written if(if(A > B, if(C > D, true, false), if(E > F, true, false)), 10px, 20px)
(or if(E > F, true)
if false
is the zero value of bool).
Does if(p)
then only accept bool for p? Do you need to write if(var(--v) != 0)
?
If bool is added, there are a lot of attributes that are either "true"/"false" or have bool-ish definitions and it would be handy to be able to do things like if(attr(aria-pressed bool))
.
the logical and comparative operators need to be added to calc expressions
Kind of, but they would return a boolean, which probably most math functions (like calc()
) shouldn't accept as an argument. So "calc expressions" may not be the right term.
if()
is essentially a handy way of converting boolean-typed calc expressions into non-bools? Doesif(p)
then only accept bool for p?
Yes, that's what I was thinking.
If bool is added, there are a lot of attributes
I wonder whether some string operations could be useful too, like checking if an attribute value starts, ends or contains a substring.
If bools are an ordinary type (not special syntax used in if()
), I don't see why calc(A < B)
shouldn't return a bool. It may not be an exceptionally useful thing to do on its own, but it can be used to parenthesize expressions within if()
, and it could later be used like --custom-houdini-func(A < B)
. There are certainly cases where using a bool wouldn't make sense like in width:
but string doesn't make sense there and they can justly fail.
For strings, to keep things consistent, I guess ^=
and $=
and friends from the []
selector could be used, though that might be a bit hairy fitting it in the grammar with *=
and the i
/s
modifiers. Additional string functions could be added later to make it and other things more convenient.
I do suspect we might add a bool type at some point for these kinds of purposes, yeah.
I'm inclined to currently put this on the side of "make sure that custom functions can handle this case so --if()
is possible.
If everything is in place except if()
wouldn't the custom func just be something like registerCustomFunc("--if", (p, t, f) => p ? t : f);
? (Ignoring the specifics of how funcs are registered and what additional metadata needs to be provided, etc.).
More or less, yeah.
Is there much use for the bool type if you can't do conditionals (without js)?
I propose three mathematical functions inspired by Iverson bracket notation — if(p), media(q), supports(s) — that can only be used within calc() and always return either 0 or 1. This is useful for concisely toggling terms on or off in an expression, like
width: calc(500px + 10px*media((width > 800px)))
.media() is simple to define by example:
is equivalent to
and similarly for supports(). If the media or feature query match, evaluate to 1. If it fails to match or is not recognized, evaluate to 0.
if() is similar but work on expressions logical and comparative expressions involving var(), env(), and attr(), like:
I am leaving this somewhat loosely defined. There are a lot of cases to consider I'm not sure what the best solution is for all of them. Should non logical/relation expressions be allowed? That is, should
if(2*var(--x))
evaluate to 1 if it the inner expression is computed to be nonzero or does there need to be an explicit comparison? An if() that contains an undefined var/env or unset attr should evaluate to 0 but should there also be another bracket function likedefined(var(--x))
? Should non-numbery variables always cause evaluating to 0 or shouldif(var(--state) = paused)
be allowed?For defined purely numeric cases, if() can be emulated with inventive combinations of arithmetic and min, max, clamp, abs, sign, round, mod, and rem but it gets fairly nasty even for relatively simple cases and even if you follow what's going on it lacks readability.
3455 was a previous attempt at an if(). It was deemed too powerful. It was different from the current proposal in two ways:
I suspect that 1 is the entirety of the argument for it being too powerful, though I am unsure. If this is case, this proposal can be extended by dropping the requirement that these must be in calc() and giving them optional parameters so that
And possibly similarly for media and supports, though that could require disallowing "," within the media query itself. Although note that if nonzero is considered true then you could leave media() and supports() unchanged and use
if(media(q), then, else)
.