w3c / css-houdini-drafts

Mirror of https://hg.css-houdini.org/drafts
https://drafts.css-houdini.org/
Other
1.84k stars 141 forks source link

[css-typed-om] Will need to handle min()/max() #359

Closed tabatkins closed 7 years ago

tabatkins commented 7 years ago

I haven't added it to the spec yet, but the CSSWG resolved to add min()/max() to the V&U spec at the Seattle 2017 f2f. This'll need a new class to represent it properly.

css-meeting-bot commented 7 years ago

The CSS Working Group just discussed CSSNumericValue, and agreed to the following resolutions:

RESOLVED: Switch to CSSMathValue-based expression tree with toSum() method for returning dictionary of unit types (as described aboce)
The full IRC log of that discussion ``` Topic: CSSNumericValue Related to https://docs.google.com/document/d/1NA020gku-tgEHMGK_8xkB6Ksfpd8ieL4z9jFoGchzbA/edit Related to https://docs.google.com/document/d/1NA020gku-tgEHMGK_8xkB6Ksfpd8ieL4z9jFoGchzbA/edit ARG WHY WON"T THIS PASTE Issue #359 Sorry, but no Tracker is associated with this channel. Github topic: https://github.com/w3c/css-houdini-drafts/issues/359 shane: We came up with a proposal over lunch shane: Getting rid of previous proposal that we resolved on shane: and replacing it with CSSNumericValue which has two subclasses: CSSMathValue and CSSUnitValue shane: CSMathValue has two members, operator and operands shane: operator is a string reprsenting a math operator like "*" shane: *, /, min, max, + shane: operands are CSSUnitValue (or presumably also other CSSMathValue??) shane: operands is a list of CSSUnitValue s/CSSUnitValue/CSSNumericValue shane: CSSNumericValue has a method called toSum which returns a map of units to numbers shane: representing a sum of those dimensions, as previously shane: If the CSSNumericValue cannot be so reduced, it either returns null or throws, whatever the TAG decides SimonSapin: If the initial syntax uses unncessary parentheses or calc() inside of calc(), do we want hte tree to preserve that information? TabAtkins: I would prefer we don't calc(1px + (2px + 3px)) => CSSMath("+", [1px, 2px, 3px]) fantasai: I agree, because people put arens when they want to clarify something, nto just when they have a grouping, e.g. calc(1px + (2*2px)) fantasai: I agree, because people put arens when they want to clarify something, nto just when they have a grouping, e.g. calc(1px + (2*2px)) xidorn: Do we want to distinguish between 3px and calc(3px)? TabAtkins: Yes, and we'll probably default it to the plus operator plinss: Going back to unnecessary parens, I think we should preserve that structure. plinss: If author wants to reduce, can use toSum() plinss: E.g. 2*3em, they did that for a reason TabAtkins: Don't have a problem with it, but would mean operator(+,[operator(+, [2px, 3px])]) shane: Any code dealing with this would have to recurse anyway Which would mean calc(1px + (2px + 3px)) => CSSMath("+", [1px, CSSMath("+", [2px, 3px])]) TabAtkins: There's no non-broken code you coudl write that would break on extra parens being grouped s/grouped/preserved/ plinss: So let's preserve struture SimonSapin: So no distinction btw nested calc() and nested parens? TabAtkins: We don't even when serializing, so no shane: What do you do when animating between calc(5px + (4em + 2%)) and calc(10px + 10em + 10%) TabAtkins: animations are computed value anyway, and so they're necessarily reduced -- they're produced by the engine, not by the author TabAtkins: The beginning point can be unit value, endpoint a calc(). What's in between? Engine decides fantasai: Tab, since you were concerned about typing, could consider CSSMathValue as a listlike type, and .op as the operator member TabAtkins: No, it's fine. SimonSapin: Earlier were talking about Proxy for arraylike stuff SimonSapin: if it's not used here, where is it used in CSSOM? TabAtkins: TransformValue and UnparsedValue SimonSapin: Why do we want to do something different here? TabAtkins: Semantically different -- this is an operator, and arguments to that operator. Those are fundamentally sequences iank_: So this proposal is completely replacing hte CSSCalcValue stuff we discused earlier? TabAtkins: yep, throw it away iank_: So you wouldn't have any of those complex units like em*ch iank_: Right TabAtkins: Still want to handle custom units whenever they exist TabAtkins: also may want to handle case-insensitivity fantasai: custom idents are case-sensitive TabAtkins: Note in the spec that we don't have custom units yet, but plan to Rossen: Sounds like ppl are happy with this? fantasai: Share Tab's earlier concern about operator and operand being perhaps a bit too long to type, but otherwise seems okay to me SimonSapin asks how percents represented TabAtkins: as "percent". Didn't use "%" so that it would be weird to use as a .name plinss: Unitless zero? TabAtkins: custom stuff can't use unitless zero, only in CSS syntax. TabAtkins: also calc() can't have unitless zero SimonSapin: I have some bugs to file then RESOLVED: Switch to CSSMathValue-based expression tree with toSum() method for returning dictionary of unit types (as described aboce) ```
tabatkins commented 7 years ago

That is, throw away CSSCalcValue, in favor of:

interface CSSMathValue {
  attribute CSSMathOperator operator;
  attribute Array arguments;
  readonly attribute DOMString type;
};

enum CSSMathOperator { "+", "*", "/", "min", "max" };

partial interface CSSNumericValue {
  record<DOMString, double> toSum();
};

toSum() is analogous to to - if possible, it collapses it down to a sum of simple units, and represents that as a JS object.


So, for example, calc(5 * (1px + 2em)) becomes CSSMath("*", [5, CSSMath("+", [1px, 2em])]). If you call .toSum() on that, you get {em: 10, px: 5}.

tabatkins commented 7 years ago

Question! What's the representation of calc(1 * 2 / 3 * 4 / 5)? Can we re-order arbitrarily to simplify things, like Math("/", [Math("*", [1, 2, 4]), 3, 5])? Or do we need to preserve ordering, like Math("*", [1, Math("/", [2, 3]), Math("*", [4, 5])])?

tabatkins commented 7 years ago

And I assume that the "/" operator can take >2 arguments, with the first argument serving as the numerator and all additional arguments being multiplied together to form the denominator? So that Math("/", [1, 2, 3]) implies calc(1 / 2 / 3). (This is the interpretation that Lisp gives to the / function.)

And I assume that 0-arguments are an error? We don't have a privileged identity value - we can't tell if Math("+", []) should resolve to a 0px, 0deg, or what. And the identify values for min/max are positive/negative infinity, which aren't CSS values.

tabatkins commented 7 years ago

Question! What's the representation of calc(1 * 2 / 3 * 4 / 5)?

Discussion with @shans and @plinss led us to two possibilities:

  1. Proper ordering, with parens, is ((((1 2) / 3) 4) / 5). So that would be the tree produced:

    Math("/", [Math("*", [Math("/", [Math("*", [1, 2]), 3]), 4]), 5])
  2. For negatives, we don't have a "-" op, we just push the negative into the value and use "+". We can do the same here; instead of a "/" op, we just have an "inverse" op, and use "*" otherwise. So the tree produced would be:

    Math("*", [1, 2, Math("inverse", [3]), 4, Math("inverse", 5)])

We prefer (2), so I'm gonna use that in the spec unless somebody objects.

surma commented 7 years ago

Did some bikeshedding w/ @tabatkins OOB: “Reciprocal” is an alternative to “inverse”. Personally, I prefer that.

tabatkins commented 7 years ago

"reciprocal" is above my comfort-level for spelling.

tabatkins commented 7 years ago

As an alternative: add a "pow" operation, and use it (with a -1 arg) to represent division.

surma commented 7 years ago

Eh, after some thinking, I’m fine with “inverse”.

add a "pow" operation

I think introducing operations that aren’t available in OG calc() is misleading.

tabatkins commented 7 years ago

By analogy with "*", I think I'll just call it "1/", actually.

And tho I don't think we'll ever build it that way, it needs to be able to accept multiple arguments (since the other operators do), which I think I'll interpret as being multiplied together before the whole thing is inverted. That is, CSSMathValue("+", 1, CSSMathValue("1/", 2, 3)) will equal calc(1 + (1 / 2 / 3)).

surma commented 7 years ago

I like “1/”, but looking at this ambiguity, doesn’t option 1 from your earlier comment seem more attractive in terms of developer experience?

tabatkins commented 7 years ago

Not particularly? I don't see why one or the other is better in terms of DevExp. Unless I go all-out and make them both take exactly two arguments, it seems like you have to handle pretty much the same sort of thing.

Starting to spec this, tho, and I'm feeling the lack of a negation operator. It's actually kinda weird to distribute the negation inward to the leaves; it makes .sub() dramatically different in effect from everything else. I think I'd prefer to add a "0-" operator too.

Maybe drop the numeric suffixes and just use "/" and "-" for consistency, if it seems clear enough. Just want to make it clear that ("-", 2, 3) == calc(-(2 + 3)) rather than calc(2 - 3). It's clearer when there's 1 arg or 3+ args, but the 2-arg case feels ambiguous with "-" and "/", but clear with "0-" and "1/".

tabatkins commented 7 years ago

And I think, rather than the engine always only outputting 1-arg sub/div, I'd go ahead and collect consecutive ones, so calc(1 / 2 / 3) becomes ("*", 1, ("/", 2, 3)) (rather than ("*", 1, ("/", 2), ("/", 3))).

tabatkins commented 7 years ago

Ugh I don't like any of this.

Okay, so refresh. There's not really any use-case for allowing authors to change the operator; I don't think there's a reasonable case for swapping from calc(1 + 2 + 3) to calc(1 * 2 * 3), for example.

So, instead, I can have six classes, one for each operation: CSSMathAdd, CSSMathMul, CSSMathNeg, CSSMathInv, CSSMathMin, and CSSMathMax. Add/mul/min/max take 1+ arguments, neg/inv take exactly one argument. Structure is like:

interface CSSMathAdd {
  attribute CSSNumericList arguments;
  readonly attribute CSSMathOperator operator;
  readonly attribute DOMString type;
}
// or
interface CSSMathNeg {
  attribute CSSNumericValue argument;
  readonly attribute CSSMathOperator operator;
  readonly attribute DOMString type;
}

operator returns "+", "-", "*", "/", "min", or "max". It's redundant with the interface itself, but makes it easier to handle in general. (For example, you can drop it into a switch statement, which you can't do as easily with a type test.)

tabatkins commented 7 years ago

I'm definitely going for that last one (separate interfaces per operator). It works better in general, and it's more future-proof; right now all our calc() operators take either 1 argument, or N identically-handled arguments, but it's possible we may add operators in the future that take, say, 2 arguments that are interpreted differently. (For example, a round() function.) In that case, it's better to have an interface that reflects the two arguments properly, as separate attributes.

tabatkins commented 7 years ago

Still a good chunk of dangling references to fix, but this is overall done now.