w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.52k stars 672 forks source link

[css-values] specification for calc() should be clearer about when the result has a percentage #10017

Open dbaron opened 9 months ago

dbaron commented 9 months ago

There are a number of cases where layout algorithms in CSS (and perhaps other things) care about whether a value has a percentage. In particular, values that have a percentage are treated differently when there's nothing for the percentage to resolve against: inside of something with auto height a height: calc(30px) is a fixed value but a height: calc(30px + 0%) is treated as height: auto.

With calc() as it was specified in css-values-3, this could essentially be implemented as part of the type computation; a calc() expression could effectively be either a <length> or a <length-percentage> as its toplevel type, and those that were <length-percentage> act as though they have a percent.

Newer levels of the specification introduce features that make this approach insufficient:

(I think this is clearly defined for calc-size() because it's very important there, but I don't think it's clearly defined for the other cases.)

It should be clearer whether these other things "erase" the percentage-ness of their arguments when they erase the types of those arguments, or whether they still produce toplevel values that are treated as having a percentage.

tabatkins commented 9 months ago

Unit division is actually well-defined, by reference to Typed OM's concept of "matching". Something matches <length>/etc only if it has {length -> 1} and its percent hint is null. I suppose technically it's only by implication that a non-null percent hint means it contains a percentage, tho, so we could probably put in a definition for that.

In-flight edit: actually it looks like I am invoking the types wrong for cases like min(); there I just say the args must have "the same type" and define that the function resolves to that type. This doesn't handle percent hints properly. I'll need to tweak it.


sign() does indeed currently erase the percentage information, tho, as does anything that returns a value of a different type than its input calculations. We should think about whether it should transfer that information thru - doing so via the calculation type (copying the percent hint of the input calculation) would mean it doesn't match <number> anymore.


The progress() family just need to define their type as the sum of their input calculation's types, same as min()/etc. (Subject to the changes I just discovered I need to make.)


calc-size() makes an end-run around the issue, since it defines that it simply acts like its basis argument in all ways.

Loirooriol commented 9 months ago

If they contain a percentage, I think they should be treated as containing a percentage, no matter how much nested into math functions it appears.

The exception should be e.g. height: mix(25%; 0px; 100px) where the 25% is not treated as per height rules, it's basically equivalent to height: calc(0.75 * 0px + 0.25 * 100px) so no percentage.

@tabatkins You are mentioning types, but percentages in height just have a «[ "length" → 1 ]» type, with null percent hint. See https://drafts.csswg.org/css-values-4/#determine-the-type-of-a-calculation

In all cases, the associated percent hint is null.

tabatkins commented 9 months ago

Yup, that's also part of what I noticed was wrong. Percents resolved against another type should gain that as a percent hint.

dbaron commented 9 months ago

Oops, I had misunderstood the statement in determine the type of a calculation that "In all cases, the associated percent hint is null" as applying to the whole section rather than just the terminal values, so I thought that the calc() spec wasn't using the percent hint mechanism. But I realize (despite the perhaps unclear indentation) that I think that's applying only to the terminal values bullet, and it's the adding types rule that creates the percent hints.

tabatkins commented 9 months ago

Yeah, I'm gonna do a quick rewrite of that bit, I also read it wrong before realizing the scope of that line. :(

tabatkins commented 9 months ago

Okay, fixed up the first two issues. After giving it some thought, I'm fairly certain that the type-changing functions should also retain the percent hint of their arguments. I think in practice it's fine; Typed OM has to worry about percentages that aren't yet resolved (thus the percent hint, which indicates what we've assumed the percent will resolve to), while in general use the calculation will know where it's used, so percents will all have a consistent type.

Still, tho, I'm gonna go ahead and define that they preserve the type hint.

dbaron commented 9 months ago

I think I also agree that the type-changing functions should preserve the percent hint, with the probable exception of calc-size(). I think https://github.com/w3c/csswg-drafts/issues/10017#issuecomment-1971995790 also agrees with that.

Is there someplace that defines that it's the percent hint that affects the layout behaviors I mentioned? I don't think defining whether the value is accepted for a <length-percentage> is alone sufficient to define that, and I didn't see it defined elsewhere.

(Also see #10018, which I filed as a separate issue since it's at least somewhat distinct.)

tabatkins commented 9 months ago

I think I also agree that the type-changing functions should preserve the percent hint,

Yup, that's now in the spec.

with the probable exception of calc-size()

What do you mean by this?

Is there someplace that defines that it's the percent hint that affects the layout behaviors I mentioned?

No, not yet. Values is probably the right place to define it, one sec.

dbaron commented 9 months ago

I mean that calc-size() should erase the percent hint that comes from the calculation part of its value (so that it doesn't have the layout effects). (I don't think this is problematic since I think calc-size() should effectively mandate that both arguments have (after applying a "length" percent hint) a type of «[ "length" → 1 ]», and the only question about its resulting type is whether there's a percent hint to propagate from the basis argument.

tabatkins commented 9 months ago

Oh, yeah, a % in the calculation argument of calc-size() shouldn't have any effect; I censor the bad cases away by making the % resolve to 0 in that case. Only a % in the basis argument matters, and that should be already handled by the spec.

Anyway, "contains a percentage" is now defined in terms of the percent hint in the value's type, and I made Sizing 3 ref that. I'm sure there are other places we need to reference this from, but I'd have to hunt them down.

tabatkins commented 9 months ago

Okay, I've finished up the edits to Values 5 as well. Making progress() correctly preserve percentage-ness into a calc-mix() was a little tricky, but it should be correct now, so width: calc-mix(progress(25% from 100px to 200px), 300px, 400px); preserves the fact that it has used a percentage. (But width: calc-mix(50%, 300px, 400p); does not.)

As far as I can tell, this is fully resolved now.

cdoublev commented 9 months ago

Should sqrt() and trigonometric functions (excluding atan2()) accept <percentage>? This does not currently appear to be the case in Chrome and FF.

I am asking this because they are defined to take calculation(s) resolving either to <number> or <angle>, and now their return type should either be consistent (the result of adding argument types) or made consistent, which I think is not usefull if they do not accept <percentage>.

I also note that log() and exp() only accept calculation(s) resolving to <number>, but their return type is not defined to be consistent or made consistent.


Typo:

A value contains a percentage if its type is type is «[ "percent" → 1 ]»

xiaochengh commented 9 months ago

Should sqrt() and trigonometric functions (excluding atan2()) accept ? This does not currently appear to be the case in Chrome and FF.

I believe percentage-ness should also be preserved in these functions. It should never be removed once added into a calculation.

The implementation in Chrome doesn't fully comply with the spec. There's no exact equivalence of percentage-ness in the impl, and it assumes that there's percentage-ness iff the result type involves percentages.

Loirooriol commented 9 months ago

@cdoublev See the explanation in https://drafts.csswg.org/css-values/#exponent-funcs "Why does hypot() allow dimensions (values with units), but pow() and sqrt() only work on numbers?"

It's not even clear what sine and friends would do with a dimension (other than an angle, which is dimensionless in math)

That said, things like height: calc(sqrt(50% / 1px) * 1px) should work, so making the type consistent preserves the percent hint.

I think Tab forgot about log() and exp().

cdoublev commented 9 months ago

Ah yes, I had forgotten that divisions like 50% / 1px match <number>. Thanks for reminding me.

cdoublev commented 8 months ago

I might be missing something again but 50% / 1px does not match <number> (as required by sqrt()) now that it has a percent hint:

A type matches <number> if it has no non-zero entries and its percent hint is null.

Loirooriol commented 8 months ago

Mm, yeah, I guess sqrt() & friends should ignore the percent hint when validating the input?

tabatkins commented 8 months ago

I also note that log() and exp() only accept calculation(s) resolving to , but their return type is not defined to be consistent or made consistent.

Whoops, that's an oversight on my part. In the source, there's a huge details block separating the log/exp definitions from the preceding pow/sqrt/hypot ones, so I just missed that they were there.

tabatkins commented 8 months ago

Re: the percent hint making things not match, I'm thinking now if I ever actually needed that restriction. What I really want to capture is the idea that the context the calculation finds itself in supplies the ability to resolve percentages (or not). I ran into this as an issue when dealing with progress()/*-mix(), and kinda manually hacked it in, but you're right that this is a larger issue.

Let me fiddle with the wording a bit.

tabatkins commented 8 months ago

Okay, yeah, I've gone ahead and loosened the text in Typed OM; it now just requires that the context in which the value is used allow percentages; if that's true, it can then match a <length> or whatever even with a non-null percent hint.

I've also tweaked <progress> in Values 5 to be consistent with this - a literal <percentage-token> used as a <progress> will map to the obvious <number>, but any other uses of percentages (aka, in a function) will resolve percentages as normal for the context.

cdoublev commented 8 months ago

The argument calculations in mod() and rem() should probably have a consistent type (like round()):

  The modulus functions <dfn lt="mod()">mod(A, B)</dfn>
  and <dfn lt=rem()>rem(A, B)</dfn>
  [...]
  The argument [=calculations=] can resolve to any <<number>>, <<dimension>>, or <<percentage>>,
- but must have the <em>same</em> [=determine the type of a calculation|type=],
+ but must have a [=consistent type=]
  or else the function is invalid;
- the result will have the same [=CSSNumericValue/type=] as the arguments.
+ the result's type will be the [=consistent type=].

The input calculations in log() should probably have a consistent type (it has two arguments, like pow()):

  The <dfn lt="log()">log(A, B?)</dfn> function
  contains one or two [=calculations=]
  [...]
  which must resolve to <<number>>s,
  and returns the logarithm base B of the value A,
  as a <<number>>
- with the return type [=made consistent=]
- with the input [=calculation’s=] type.
+ The input [=calculations=]
+ must have a [=consistent type=]
+ or else the function is invalid;
+ the result's type will be the [=consistent type=].

The return value type of random() should probably be consistent.

  All of the [=calculation=] arguments can resolve to any <<number>>, <<dimension>>, or <<percentage>>,
- but must have the <em>same</em> [=determine the type of a calculation|type=], or else the function is invalid;
- the result will have the same type as the arguments.
+ but must have a [=consistent type=], or else the function is invalid;
+ the result's type will be the [=consistent type=].

And <percentage> should probably be <percentage-token> in the production rule of <progress> (which should probably only allow omitting easing).

- <progress> = [ <percentage> | <number> | <'animation-timeline'> ]? && by <easing-function>
+ <progress> = [ <percentage-token> | <number> | <'animation-timeline'> ] [ by <easing-function> ]?
cdoublev commented 8 months ago

My first thought was that percentHint should not be checked at all (when matching a standalone dimension type or <number>), but I was not sure of the consequences.

I did not considered that the context should supply the ability to resolve percentages as a valid reason, because I thought hsl(calc(var(--progress-as-percentage, 0%) / 100%) 100 50) would be a valid use case, and it seemed inconsistent to allow hsl(calc(1px / 1px) 100 50) but not hsl(calc(1% / 1%) 100 50).

edit: hmm, no, there is no percent hint involved in my example, which is valid, but I do not see when a percent hint can appear when the context does not allow it.

dbaron commented 8 months ago

An expression having a percent hint means that the expression only makes sense if percentages in that expression are the type of the percent hint. For example, calc(10px + 20%) has a percent hint of length and a type of «[ "length" → 1 ]» (the type for <length>), while calc((10px + 20%) / 15px) still has a percent hint of length but it has a type of «[ ]» (the type for <number>).

So if we don't check percent hints at all for values of type <number> then calc((10px + 20%) / 15px) would be accepted for any <number>, whereas it only really makes sense in a context that both (a) accepts <number> and (b) where percentages resolve to lengths.

dbaron commented 8 months ago

Another relevant example showing something that we do want to accept is width: calc(log((10px + 20%) / 15px) * 3em). The log() function accepts only <number> and in this case it is being used in a context where percentages are lengths. So log() allows the number it accepts to have a percent hint, and the percent hint needs to be present on the result type of log() as well, so that whatever contains the log() will eventually check that the context is one where percentages are lengths.

Loirooriol commented 8 months ago

I think the presence of a percent hint should indicate that the context allows resolving percentages as that type.

So in border-width: calc(log((10px + 20%) / 15px) * 3em) it doesn't make sense for 10px + 20% to have type «[ "length" → 1 ]» with a length percent hint. "add two types" should just return failure because border-width doesn't resolve percentages as lengths.

cdoublev commented 8 months ago

(Thanks for the examples. I missed that a percentage can get a percent hint when it is added to a dimension type.)

Returning failure when adding a percentage type without a percent hint to anything but a percentage without a percent hint, also makes sense to me.

edit: no because "when you're doing operations on plain CSSNumericValue objects, you don't yet know the context they'll be used in, so you can't tell whether a % will become a length/etc" (https://github.com/w3c/csswg-drafts/pull/10727#issuecomment-2295335895).