tabatkins / css-toggle

Proposal for a CSS Toggle spec
Creative Commons Zero v1.0 Universal
28 stars 1 forks source link

clarify how toggles are supposed to be updated, and whether it should all happen in a single flush #27

Open dbaron opened 2 years ago

dbaron commented 2 years ago

Toggle Creation Details currently says:

ISSUE 1: Define the precise point in update the rendering when toggles are created if toggle-root names a toggle that doesn’t exist on the element yet.

It's not clear to me what the intent of this.

Done naively, I think this would produce the result that :toggle() pseudo-classes don't match until the update after the toggle-root property is added. I think this would have two harmful results:

  1. you'd see a flash of "unstyled" content where the :toggle() selectors weren't matched on the first update
  2. Things that "flush" style wouldn't produce up-to-date results for the matching of :toggle() selectors, in cases where the toggle-root property is new (which, in realistic cases, means the element was newly created and the flush is styling it for the first time).

I presume the alternative (within the description given in the issue) is to create the toggles and then do another round of restyling to match the :toggle() selectors, and repeat until stable. I think the number of possible iterations is bounded by the number of elements in the document... though in realistic cases more than one iteration seems unlikely. This does require adding a loop in a relatively core part of the engine for something that's specific to toggles, and it may require some care to specify how it interacts with other features.

I think there's a third alternative that deviates from what is suggested in the issue, which is to work like transitions and create the toggles during style resolution, before resolving style on descendants or later siblings. This is problematic for engines that want to do some of this work in parallel, and it also introduces a lot of complexity. (Getting this right for transitions is hard, and I'm not sure we've done so yet. We recently proposed redesigning how it works.) But this does avoid the problems with either of the previous two alternatives.

I'm curious which alternative was actually intended by the spec.

tabatkins commented 2 years ago

So, the spec intent was somewhat vague, because I hadn't yet looked in detail at the "update the rendering" algo to figure out what I wanted. It just seemed like the right place to do the update.

Fundamentally, while this stuff isn't cyclic, some degree of cycling is necessary to reach stability. I figured the "one frame delayed" was by far the easiest impl-wise (aka the "naive" approach you outline), but it does indeed have some less-great outcomes for authors, particularly if you create some toggles as a response to other toggles. But even the single-shot "new element added to the document with a toggle-root property on it" has the issues, which is unfortunate.

Hmm, I wonder if we could/should specify that we do one round of re-running selectors when new toggles are created as a result of style application? That strictly limits the potential perf damage while still fixing the 90% case, and other features could hook into the same functionality to trigger the "one rerun only" behavior. Chained toggle creation would still spread across multiple frames, but as a rare/weird case I think that would be ok?

Just thinking out loud, what's the intended mechanism to handle style CQs? Would they have the same issue? Does their scoping (vs toggles' somewhat wider counter-like scoping) affect this? Does knowing about the CQ containers ahead of time make things different (vs toggles being possible to appear on any element at any time)?

andruud commented 2 years ago

I'm not sure this whole feature can fit well into CSS, and I'm not sure we should even be implementing it in Blink. But disregarding that:

you'd see a flash of "unstyled" content where the :toggle() selectors weren't matched on the first update

Hmm, I wonder if we could/should specify that we do one round of re-running selectors

This reminds me a lot of the "first frame problem" from @scroll-timeline (later reworked to a scroll-timeline property). The problem was the following:

Hence you'd get a flash of "unstyled" content the first frame.

The solution there was to re-run the style and layout lifecycle phases (at most once) if the first style/layout phase resulted in any "new" ScrollTimeline objects. I tried to spec this at some point (PR). Although I don't really like this solution. It's not sustainable to add extra passes for everything.

This does require adding a loop in a relatively core part of the engine for something that's specific to toggles

If it helps, we can (and should) share that loop with ScrollTimeline. (We currently repeat at the lifecycle level for ScrollTimeline, but I'm not sure that's what we really want, since it makes gCS results weird).

how it interacts with other features.

Things that come to mind:

Just thinking out loud, what's the intended mechanism to handle style CQs? Would they have the same issue? Does their scoping (vs toggles' somewhat wider counter-like scoping) affect this?

They don't have the same issue, because only descendants can query the style of an element. (An element can not query itself).

Does knowing about the CQ containers ahead of time make things different (vs toggles being possible to appear on any element at any time)?

But we don't actually know about CQ containers ahead of time(?). (Depending on how far "ahead" you mean I suppose).

tabatkins commented 2 years ago

If it helps, we can (and should) share that loop with ScrollTimeline. (We currently repeat at the lifecycle level for ScrollTimeline, but I'm not sure that's what we really want, since it makes gCS results weird).

Right, that's what I intended by "and other features could hook into the same functionality to trigger the 'one rerun only' behavior". If we're already doing this "one rerun if condition X is hit", then lets' just piggyback them!

Having N features all triggering independent reruns is definitely bad; it reduces the perf benefits, and it means you can chain some things multiple levels deep, depending on rerun-evaluation order, but not others. That's not great design, as it's opaque to authors. Having them all trip the same re-run switch is much better.

We might want to specify that all the passes behave as one "style change event" for the purpose of animations/transition updates. (Meaning that you can't get e.g. transition events from intermediate states).

Definitely, kicking off an animation based on a transient state that's never page-observed otherwise would be bad.

Container Queries may mean that these extra "style" passes are really style and layout passes. (Just saying it's possible [not necessarily likely] to make this very expensive).

Ugh, but yeah, not necessarily likely, and CQs are always expensive in the first place.

ResizeObserver is currently specified to do multiple passes on the lifecycle level. Should specify whether our passes happen within each of those passes (which is needed for gCS consistency), or something else.

Hm, I'll need to look at this more. But each pass is needed because previous passes can update style in a way that affects the size of descendants, so yeah, I presume we'd want the same level of consistency you'd get otherwise. (So do the reruns on these passes if needed, too.) (And again, this would only matter if you're creating toggles, or scroll timelines I suppose, based on container sizes, which seems about as unlikely as doing it in a CQ.)

andruud commented 2 years ago

If it helps, we can (and should) share that loop with ScrollTimeline.

If we're already doing this "one rerun if condition X is hit", then lets' just piggyback them!

Actually ... I'm not sure it's that simple. What if:

(Assume it's the very first frame):

I suppose the reverse it also possible. (Toggles triggering the extra pass, and then timelines don't get a second chance).

In fact, it's a problem even just for scroll-timeline alone. When the multi-pass thing was proposed for scroll-timeline, container queries didn't exist yet, so there was no way for effects of scroll-linked animations to cause new timelines.

;_;

tabatkins commented 2 years ago

Right, it's definitely possible to set up a chain of arbitrary length that, while not cyclic, can require a large number of reruns before reaching stability. My point is just that (a) most of the time the chain is length 1, so a single rerun is fine, and (b) most of the time there is a length-1 chain, so a single rerun is needed, and (c) even when the chains are longer they'll be consistent eventually, in later frames. Thus, a single rerun should suffice to make most cases work well, without crippling things otherwise.