Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
dbaron opened this issue Feb 29, 2024 · 26 comments
Labels

Comments

@dbaron
Copy link
Member

dbaron commented Feb 29, 2024

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
Copy link
Member

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
Copy link
Contributor

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
Copy link
Member

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

@dbaron
Copy link
Member Author

dbaron commented Feb 29, 2024

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
Copy link
Member

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

@tabatkins
Copy link
Member

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
Copy link
Member Author

dbaron commented Feb 29, 2024

I think I also agree that the type-changing functions should preserve the percent hint, with the probable exception of calc-size(). I think #10017 (comment) 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 added a commit that referenced this issue Feb 29, 2024
@tabatkins
Copy link
Member

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
Copy link
Member Author

dbaron commented Feb 29, 2024

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
Copy link
Member

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
Copy link
Member

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
Copy link
Collaborator

cdoublev commented Mar 1, 2024

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
Copy link
Contributor

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
Copy link
Contributor

Loirooriol commented Mar 1, 2024

@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, with 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
Copy link
Collaborator

cdoublev commented Mar 1, 2024

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

@cdoublev
Copy link
Collaborator

cdoublev commented Mar 3, 2024

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
Copy link
Contributor

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

@tabatkins
Copy link
Member

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
Copy link
Member

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 added a commit that referenced this issue Mar 4, 2024
…percentage tokens to be equivalent to a number. #10017
@tabatkins
Copy link
Member

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
Copy link
Collaborator

cdoublev commented Mar 5, 2024

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
Copy link
Collaborator

cdoublev commented Mar 5, 2024

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
Copy link
Member Author

dbaron commented Mar 5, 2024

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
Copy link
Member Author

dbaron commented Mar 5, 2024

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
Copy link
Contributor

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
Copy link
Collaborator

cdoublev commented Mar 6, 2024

(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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants