Skip to content

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

Open

Description

@dbaron

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.

Activity

  1. tabatkins commented on Feb 29, 2024

    @tabatkins
    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.

  2. Loirooriol commented on Feb 29, 2024

    @Loirooriol
    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.

  3. tabatkins commented on Feb 29, 2024

    @tabatkins
    Member

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

  4. dbaron commented on Feb 29, 2024

    @dbaron
    MemberAuthor

    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.

  5. tabatkins commented on Feb 29, 2024

    @tabatkins
    Member

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

  6. tabatkins commented on Feb 29, 2024

    @tabatkins
    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.

  7. dbaron commented on Feb 29, 2024

    @dbaron
    MemberAuthor

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

  8. tabatkins commented on Feb 29, 2024

    @tabatkins
    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.

  9. dbaron commented on Feb 29, 2024

    @dbaron
    MemberAuthor

    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.

  10. 8 remaining items

  11. Loirooriol commented on Mar 4, 2024

    @Loirooriol
    Contributor

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

  12. tabatkins commented on Mar 4, 2024

    @tabatkins
    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.

  13. tabatkins commented on Mar 4, 2024

    @tabatkins
    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.

  14. tabatkins commented on Mar 4, 2024

    @tabatkins
    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.

  15. cdoublev commented on Mar 5, 2024

    @cdoublev
    Collaborator

    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> ]?
  16. cdoublev commented on Mar 5, 2024

    @cdoublev
    Collaborator

    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.

  17. dbaron commented on Mar 5, 2024

    @dbaron
    MemberAuthor

    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.

  18. dbaron commented on Mar 5, 2024

    @dbaron
    MemberAuthor

    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.

  19. Loirooriol commented on Mar 5, 2024

    @Loirooriol
    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.

  20. cdoublev commented on Mar 6, 2024

    @cdoublev
    Collaborator

    (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" (#10727 (comment)).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions