diff --git a/box-tree-api/Overview.bs b/box-tree-api/Overview.bs index 64a2419a..2b71cd5b 100644 --- a/box-tree-api/Overview.bs +++ b/box-tree-api/Overview.bs @@ -72,6 +72,7 @@ Boxes are not explicitly exposed by this API.

API

+[Exposed=Window]
 interface DeadFragmentInformation {
 	readonly attribute Node node;
 	readonly attribute double width;
@@ -79,7 +80,7 @@ interface DeadFragmentInformation {
 	readonly attribute double top;
 	readonly attribute double left;
 	readonly attribute boolean isOverflowed;
-	readonly attribute sequence<DeadFragmentInformation>? children;
+	readonly attribute FrozenArray<DeadFragmentInformation>? children;
 	readonly attribute DeadFragmentInformation? nextSibling;
 	readonly attribute DeadFragmentInformation? previousSibling;
 	readonly attribute DeadFragmentInformation? nextInBox;
diff --git a/css-animation-worklet-1/Makefile b/css-animation-worklet-1/Makefile
new file mode 100644
index 00000000..63fe58ab
--- /dev/null
+++ b/css-animation-worklet-1/Makefile
@@ -0,0 +1,20 @@
+# $Id: Makefile,v 1.5 2008/02/06 14:05:15 mike Exp $
+#
+# FIXME: New documentation needed.
+#
+# Use "make REMOTE=1" to use remote bikeshed
+
+SOURCEFILE=Overview.bs
+OUTPUTFILE=Overview.html
+PREPROCESSOR=bikeshed
+REMOTE_PREPROCESSOR_URL=https://api.csswg.org/bikeshed/
+
+all: $(OUTPUTFILE)
+
+$(OUTPUTFILE): $(SOURCEFILE)
+ifneq (,$(REMOTE))
+	curl $(REMOTE_PREPROCESSOR_URL) -F file=@$(SOURCEFILE) > "$@"
+else
+	$(PREPROCESSOR) -f spec "$<" "$@"
+endif
+
diff --git a/css-animation-worklet-1/Overview.bs b/css-animation-worklet-1/Overview.bs
new file mode 100644
index 00000000..4f883932
--- /dev/null
+++ b/css-animation-worklet-1/Overview.bs
@@ -0,0 +1,1286 @@
+
+Title:  CSS Animation Worklet API
+Status: ED
+Group: houdini
+ED: https://drafts.css-houdini.org/css-animation-worklet-1/
+TR: https://www.w3.org/TR/css-animation-worklet-1/
+Shortname: css-animation-worklet
+Level: 1
+Abstract:
+Editor: Majid Valipour, majidvp@google.com, w3cid 81464
+Editor: Robert Flack, flackr@chromium.org, w3cid 98451
+Editor: Stephen McGruer, smcgruer@chromium.org, w3cid 96463
+Ignored Terms: AnimationWorklet
+
+ + + +
+urlPrefix: https://heycam.github.io/webidl/; type: dfn;
+    text: NotSupportedError
+    urlPrefix: #dfn-;
+        text: callback this value
+        text: exception
+        text: throw
+        url: throw; text: thrown
+    urlPrefix: #;
+        url: Function; text: Function
+        url: VoidFunction; text: VoidFunction
+    url: invoke-a-callback-function; text: Invoke
+    url: construct-a-callback-function; text: constructing
+    url: es-type-mapping; text: converting
+urlPrefix: https://html.spec.whatwg.org/#; type: dfn;
+    url: run-the-animation-frame-callbacks; text: running the animation frame callbacks
+urlPrefix: http://w3c.github.io/html/infrastructure.html#; type: dfn;
+    text: structuredserialize
+    text: structureddeserialize
+urlPrefix: https://www.w3.org/TR/css3-transitions/#; type: dfn;
+    text: animatable properties
+urlPrefix: https://drafts.csswg.org/web-animations#; type: dfn;
+    url: the-documents-default-timeline; text: default document timeline
+    url: concept-animation; text: animation
+    text: effect value
+    text: effect stack
+    text: target property
+    text: timeline
+    text: animation effect
+    text: current time
+    text: local time
+    text: inherited time
+    text: ready
+    text: play state
+    text: playback rate
+    text: set the target effect of an animation
+    text: set the timeline of an animation
+    text: finished
+    text: idle
+    text: paused
+    text: pending
+    text: running
+    text: composite operation
+    text: animation class
+    text: replace state
+    text: active
+    text: persisted
+    text: removed
+    text: start delay
+    text: end delay
+    text: fill mode
+    text: iteration start
+    text: iteration count
+    text: iteration duration
+    text: playback direction
+    text: timing function
+    text: set the start time
+    text: set the current time
+    text: update the timing properties of an animation effect
+
+
+
+urlPrefix: https://w3c.github.io/web-animations/level-2/#;
+    type: dfn;
+        text: group effect
+        text: child effect
+urlPrefix: https://tc39.github.io/ecma262/#sec-; type: dfn;
+    text: IsCallable
+    text: IsConstructor
+    text: HasProperty
+    url: ecmascript-data-types-and-values; text: Type
+    url: map-objects; text:map object
+    url: get-o-p; text: Get
+    url: set-o-p-v-throw; text: Set
+    url: samevalue; text: SameValue
+    urlPrefix: native-error-types-used-in-this-standard-
+        text: TypeError
+urlPrefix: https://www.w3.org/TR/hr-time-2/#dom-; type: dfn
+    text: DOMHighResTimeStamp
+urlPrefix: https://wicg.github.io/scroll-animations/#; type: interface
+    url: scrolltimeline; text: ScrollTimeline
+    url: dictdef-scrolltimelineoptions; text: ScrollTimelineOptions
+    url: dom-scrolltimeline-scrollsource; text: scrollSource
+urlPrefix: https://wicg.github.io/scroll-animations/#; type: dfn
+    url: current-time-algorithm; text: current time of the ScrollTimeline;
+
+ +
+{
+    "explainer": {
+        "href": "https://github.com/w3c/css-houdini-drafts/blob/master/css-animation-worklet-1/README.md",
+        "title": "Animation Worklet Explainer",
+        "deliveredBy": [
+            "https://github.com/w3c/css-houdini-drafts"
+        ]
+    },
+    "principles": {
+        "href": "https://github.com/w3c/css-houdini-drafts/blob/master/css-animation-worklet-1/principles.md",
+        "title": "Animation Worklet Design Principles and Goals",
+        "deliveredBy": [
+            "https://github.com/w3c/css-houdini-drafts"
+        ]
+    }
+}
+
+ +Introduction {#intro} +===================== +This section is not normative. + +This document introduces a new primitive that provides extensibility in web animations and enables +high performance interactive procedural animations on the web. For details on the rationale and +motivation see both [[explainer]] and [[principles]]. + +The [=Animation Worklet=] API provides a method to create scripted animations that control a set +of [=animation effects=]. The API is designed to make it possible for user agents to run such +animations in their own dedicated thread to provide a degree of performance isolation from main +thread. + +Relationship to the Web Animations API {#relationship-to-web-animations} +------------------------------------------------------------------------ +This section is not normative. + +Animations running inside an [=Animation Worklet=] execution context expose the {{Animation}} +interface from the Web Animations specification on the main javascript execution context. This means +they can be controlled and inspected from main thread using the same Web Animation APIs. + + +Animation Worklet {#animation-worklet-desc} +=========================================== +Animation Worklet is a {{Worklet}} responsible for all classes related to custom +animations. The worklet can be accessed via {{animationWorklet}} attribute. + + +[Exposed=Window] +partial namespace CSS { + [SameObject] readonly attribute Worklet animationWorklet; +}; + + +The {{animationWorklet}}'s [=worklet global scope type=] is {{AnimationWorkletGlobalScope}}. + +{{AnimationWorkletGlobalScope}} represents the global execution context of {{animationWorklet}}. + + +[ Global=(Worklet,AnimationWorklet), Exposed=AnimationWorklet ] +interface AnimationWorkletGlobalScope : WorkletGlobalScope { + undefined registerAnimator(DOMString name, AnimatorInstanceConstructor animatorCtor); +}; + +callback AnimatorInstanceConstructor = any (any options, optional any state); + + + +Animator {#animator-desc} +========================= + +An Animator represents a custom animation that is running inside +{{AnimationWorkletGlobalScope}}. Each Animator is associated with an [=animation=] instance (of +type {{WorkletAnimation}}) in the document and determines how that animation progresses its +[=animation effect=]. The animate function contains the logic responsible for +translating the animation current time into appropriate progress of the animation effect. An +animator can only be instantiated by construction of a {{WorkletAnimation}} in the document. + + +Two animators types are supported: [=Stateless Animator=] and [=Stateful Animator=] each +providing a different state management strategy. + + +Stateless Animator {#stateless-animator-desc} +--------------------------------------------- + +A Stateless Animator is a type of animator does not depend on any local state either +stored on the instance or global scope. Effectively, the animate function of an +[=Stateless Animator=] can be treated as a pure function with the expectation that given the same +input, it produces the same output. + + +
+ This is how an stateless animator class should look. +
+        class FooAnimator {
+            constructor(options) {
+                // Called when a new animator is instantiated.
+            }
+            animate(currentTime, effect) {
+                // Animation frame logic goes here.
+            }
+        }
+    
+
+ +Note: The statelessness allows animation worklet to perform optimization such as producing multiple +animation frames in parallel, sharing a single animator instance for multiple animations, and +performing very cheap teardown and setup. Using [=Stateless Animator=] is highly recommended to +enable such optimizations. + +Stateful Animator {#stateful-animator-desc} +------------------------------------------- + +A Stateful Animator is a type of animator that can have local state and animation worklet +guarantees that it maintains this state as long as the stateful animator fulfills the contract +required by its interface and as described following. + + +[=Animation worklet=] maintains a set of {{WorkletGlobalScope}}s which may exist across different +threads or processes. Animation worklet may temporarily terminate a global scope (e.g., to preserve +resources) or move a running [=animator instance=] across different global scopes (e.g., if its +effect is mutable only in a certain thread). Animation worklet guarantees that a stateful animator +instance's state is maintained even if the instance is respawned in a different global scope. + +The basic mechanism for maintaining the state is that the animation worklet snapshots the local +state that is exposed via the [=state function=] and then reifies it so that it can be passed into +the constructor when the animator instance is respawned at a later time in a potentially different +global scope. The [=migrate an animator instance=] algorithm specifies this process in details. + +A user-defined stateful animator is expected to fulfill the required contract which is that its +state function returns an object representing its state that can be serialized using structured +serialized algorithm and that it can also recreate its state given that same object passed to its +constructor. + +
+ This is how a stateful animator class should look. +
+        class BarAnimator {
+            constructor(options, state) {
+              // Called when a new animator is instantiated (either first time or after being respawned).
+              this.currentVelocity  = state ? state.velocity : 0;
+            }
+            animate(currentTime, effect) {
+                // Animation frame logic goes here and can rely on this.currentVelocity.
+                this.currentVelocity += 0.1;
+            }
+            state() {
+              // The returned object should be serializable using structured clonable algorithm.
+              return {
+                velocity: this.currentVelocity;
+              }
+            }
+        }
+    
+
+ + +Animator Definition {#animator-definition-desc} +----------------------------------------------- + +An animator definition is a [=struct=] which describes the author defined custom +animation logic. It consists of: + + - : animator name + :: A <>#. + + - : class constructor + :: A {{AnimatorInstanceConstructor}} [=callback function=] type. + + - : animate function + :: A [=Function=] [=callback function=] type. + + - : state function + :: A [=Function=] [=callback function=] type. + + - : stateful flag + :: A boolean flag + + +A stateful animator definition is an [=animator definition=] whose +[=animator definition/stateful flag=] is true. + + +A document animator definition is a [=struct=] which describes the information needed by +the [=document=] about the author defined custom animation. It consists of: + + - : stateful flag + :: A boolean flag + +Registering an Animator Definition {#registering-animator-definition} +--------------------------------------------------------------------- +The [=document=] has a [=map=] of document animator definitions. The map gets populated +when {{registerAnimator(name, animatorCtor)}} is called. + +An {{AnimationWorkletGlobalScope}} has a [=map=] of animator definitions. The map gets +populated when {{registerAnimator(name, animatorCtor)}} is called. + +Note that to register a [=stateful animator definition=] it is simply enough for the registered +class to have a state function. + +
+ +When the registerAnimator(|name|, |animatorCtor|) +method is called in a {{AnimationWorkletGlobalScope}}, the user agent must run the +following steps: + + 1. If |name| is not a valid <>, [=throw=] a [=TypeError=] and abort all these + steps. + + 2. Let |animatorDefinitions| be the {{AnimationWorkletGlobalScope}}'s + [=animator definitions=] [=map=]. + + 3. If |animatorDefinitions|[|name|] [=map/exists=], [=throw=] a [=NotSupportedError=] + and abort all these steps. + + 4. If the result of [=IsConstructor=](|animatorCtor|) is false, [=throw=] a + [=TypeError=] and abort all these steps. + + 5. Let |prototype| be the result of [=Get=](|animatorCtor|, "prototype"). + + 6. Let |animateFuncValue| be the result of [=Get=](|prototype|, "animate"). + + 7. Let |animateFunc| be the result of [=converting=] |animateFuncValue| to the [=Function=] + [=callback function=] type. If an exception is thrown, rethrow the exception and abort + all these steps. + + 8. Let |stateFuncValue| be the result of [=Get=](|prototype|, "state"). + + 9. Let |stateFunc| be the result of [=converting=] |stateFuncValue| to the [=Function=] + [=callback function=] type, If an exception is thrown, set |stateful| to be false, + otherwise set |stateful| to be true and |stateFunc| to be undefined. + + 10. Let |definition| be a new [=animator definition=] with: + + - [=animator name=] being |name| + + - [=class constructor=] being |animatorCtor| + + - [=animate function=] being |animateFunc| + + - [=state function=] being |stateFunc| + + - [=animator definition/stateful flag=] being |stateful| + + + 9. [=map/set=] the |animatorDefinitions|[|name|] to |definition|. + + 10. [=Queue a task=] to run the following steps: + + 1. Let |documentAnimatorDefinitions| be the associated [=document's=] + [=document animator definitions=] [=map=]. + + 2. Let |documentDefinition| be a new [=document animator definition=] with: + + - [=animator definition/stateful flag=] being |stateful| + + 3. If |documentAnimatorDefinitions|[|name|] [=map/exists=], run the following steps: + + 1. Let |existingDocumentDefinition| be the result of [=map/get=] + |documentAnimatorDefinitions|[|name|]. + + 2. If |existingDocumentDefinition| is "invalid", abort all these steps. + + 3. If |existingDocumentDefinition| and |documentDefinition| are not equivalent, (that is + their [=document animator definition/stateful flag=]s are + different), then: + + [=map/set=] |documentAnimatorDefinitions|[|name|] to "invalid". + + Log an error to the debugging console stating that the same class was registered + with different stateful flag. + + 4. Otherwise, [=map/set=] |documentAnimatorDefinitions|[|name|] to + |documentDefinition|. + +
+ + +Animator Effect {#animator-effect-desc} +--------------------------------------- + +A Animator Effect represents the underlying [=animation effect=] inside animation +worklet. + +It has a corresponding effect property which is a reference to the underlying +[=animation effect=]. It also has corresponding properties for the following +[=animation effect=]'s properties: + * [=local time=], + * [=start delay=], + * [=end delay=], + * [=fill mode=], + * [=iteration start=], + * [=iteration count=], + * [=iteration duration=], + * [=playback direction=], and + * [=timing function=]. + +[=Animator Effect=] is represented by the {{WorkletAnimationEffect}} interface +inside {{AnimationWorkletGlobalScope}}. + + + +[ Exposed=AnimationWorklet ] +interface WorkletAnimationEffect { + EffectTiming getTiming(); + ComputedEffectTiming getComputedTiming(); + attribute double? localTime; +}; + + + +Note: {{WorkletAnimationEffect}} is basically a restricted version of {{AnimationEffect}} interface + which does not have {{AnimationEffect/updateTiming}} but additionally allows local time to be set. + +
+ +: getTiming() +:: Returns the specified timing properties using the corresponding properties. + +: getComputedTiming() +:: Returns the calculated timing properties using the corresponding properties. + +: localTime +:: Getting the attribute returns the corresponding [=local time=]. + Setting the attribute updates the local time given this effect as |effect| + and the attribute value as |time|: + + 1. If the |time| is the same as |effect|'s [=local time=] then skip following steps. + + 2. Set the |effect|'s [=local time=] to |time|. + + 3. Set the |effect|'s animator instance's [=sync requested flag=] to true. + +
+ + +Animator Instance {#animator-instance-section} +============================================== + +An animator instance is a [=struct=] which describes a fully realized custom +[=animator=] in an {{AnimationWorkletGlobalScope}}. It has a reference to an +[=animator definition=] and owns the instance specific state such as animation effect and +timeline. It consists of: + + - : [=animator name=] + :: A string used to identify the animator definition. + + - : [=frame requested flag=] + :: A boolean flag that indicates if the animator needs to animate. + + - : sync requested flag + :: A flag that indicates if the animator needs to sync its output. + + - : effect + :: An [=Animator Effect=]. + + - : animator current time + :: A time value equivalent to the corresponding [=worklet animation=]'s current time. + + - : animator timeline + :: The [=timeline=] of the corresponding [=worklet animation=]. + + - : animator serialized options + :: The serializable object representing the options to be used when constructing the animator + instance. + +A stateful animator instance is an [=animator instance=] whose corresponding +definition is a [=stateful animator definition=]. + + + +Creating an Animator Instance {#creating-animator-instance} +----------------------------------------------------------- + +Each [=animator instance=] lives in an {{AnimationWorkletGlobalScope}}. + +Each {{AnimationWorkletGlobalScope}} has an animator instance set. The set is populated +when the user agent constructs a new [=animator instance=] in the {{AnimationWorkletGlobalScope}} +scope. Each [=animator instance=] corresponds to a worklet animation in the document scope. + +
+ +To create a new animator instance given a |name|, |timeline|, |effect|, +|serializedOptions|, |serializedState|, and |workletGlobalScope|, the user agent must run +the following steps: + + 1. Let the |definition| be the result of looking up |name| on the |workletGlobalScope|'s + [=animator definitions=]. + + If |definition| does not exist abort the following steps. + + 2. Let |animatorCtor| be the [=class constructor=] of |definition|. + + 3. Let |options| be [=StructuredDeserialize=](|serializedOptions|). + + 4. Let |state| be [=StructuredDeserialize=](|serializedState|). + + 5. Let |animatorInstance| be the result of [=constructing=] |animatorCtor| with + «|options|, |state|» as arguments. If an exception is thrown, rethrow the exception and + abort all these steps. + + 6. Let |animatorEffect| be the result of [=constructing=] a {{WorkletAnimationEffect}} + with its [=corresponding effect=] being |effect|. + + 7. Set the following on |animatorInstance| with: + - [=animator name=] being |name| + - [=frame requested flag=] being false + - [=sync requested flag=] being false + - [=animator current time=] being unresolved + - [=effect=] being |animatorEffect| + - [=animator timeline=] being |timeline| + - [=animator serialized options=] being |options| + + 8. Add |animatorInstance| to |workletGlobalScope|'s [=animator instance set=]. + +
+ + +Running Animators {#running-animators} +-------------------------------------- + +When the user agent wants to produce a new animation frame, if for any [=animator instance=] the +associated [=frame requested flag=] is true then the the user agent must +[=run animators=] for the current frame in all its associated global scopes. + +Note: The user agent is not required to run animations on every visual frame. It is legal to defer + generating an animation frame until a later frame. This allow the user agent to + provide a different service level according to their policy. + +
+ +When the user agent wants to run animators in a given |workletGlobalScope|, it +must run the following steps: + + 1. Iterate over all [=animator instance=]s in the |workletGlobalScope|'s animator instance + set. For each such |animator| the user agent must perform the following steps: + + 1. Let |animatorName| be |animator|'s [=animator name=] + + 2. Let the |definition| be the result of looking up |animatorName| on the + |workletGlobalScope|'s [=animator definitions=]. + + If |definition| does not exist then abort the following steps. + + 3. If the [=frame requested flag=] for |animator| is false or the effect belonging + to the |animator| will not be visible within the visual viewport of the current frame + the user agent may abort all the following steps. + + Issue: Consider giving user agents permission to skip running individual animator + instances to throttle slow animators. + + 4. Let |animateFunction| be |definition|'s [=animate function=]. + + 5. Let |currentTime| be [=animator current time=] of |animator|. + + 6. Let |effect| be [=effect=] of |animator|. + + 7. [=Invoke=] |animateFunction| with arguments «|currentTime|, |effect|», + and with |animator| as the [=callback this value=]. + + 2. If any [=animator instance=]s in the |workletGlobalScope|'s [=animator instance set=] + has its [=sync requested flag=] set to true then [=sync local times to document=] + given |workletGlobalScope|. + +
+ +Note: Although inefficient, it is legal for the user agent to [=run animators=] multiple times +in the same frame. + + +Issue: should be explicit as to what happens if the animateFunction throws an exception. At least +we should have wording that the localTime values of the effects are ignored to avoid incorrect +partial updates. + +Removing an Animator Instance {#removing-animator} +-------------------------------------------------- + +
+ +To remove an animator instance given |animator| and |workletGlobalScope| the user agent +must run the following steps: + +1. Remove |animator| from |workletGlobalScope|'s [=animator instance set=]. + +
+ + +Migrating an Animator Instance {#migrating-animator} +---------------------------------------------------- + +The migration process allows [=stateful animator instance=] to be migrated to a different +{{AnimationWorkletGlobalScope}} without losing their local state. + +
+ +To migrate an animator instance from one {{AnimationWorkletGlobalScope}} to another, +given |animator|, |sourceWorkletGlobalScope|, |destinationWorkletGlobalScope|, the user agent +must run the following steps : + + 1. Let |serializedState| be undefined. + + 2. [=Queue a task=] on |sourceWorkletGlobalScope| to run the following steps: + + 1. Let |animatorName| be |animator|'s [=animator name=] + + 2. Let |definition| be the result of looking up |animatorName| on |sourceWorkletGlobalScope|'s + [=animator definitions=]. + + If |definition| does not exist then abort the following steps. + + 3. Let |stateful| be the [=animator definition/stateful flag=] of |definition|. + + 4. If |stateful| is false then abort the following steps. + + 5. Let |stateFunction| be |definition|'s [=state function=]. + + 6. Let |state| be the result of [=Invoke=] |stateFunction| with |animator| as the + [=callback this value=]. If any exception is thrown, rethrow the exception and abort + the following steps. + + 7. Set |serializedState| to be the result of [=StructuredSerialize=](|state|). + If any exception is thrown, then abort the following steps. + + 8. Run the procedure to [=remove an animator instance=] given |animator|, and + |sourceWorkletGlobalScope|. + + 2. Wait for the above task to complete. If the task is aborted, abort the following steps. + + 3. [=Queue a task=] on |destinationWorkletGlobalScope| to run the following steps: + + 1. Run the procedure to [=create a new animator instance=] given: + - The |animator|'s [=animator name=] as name. + - The |animator|'s [=animator timeline=] as timeline. + - The |animator|'s [=effect=] as effect. + - The |animator|'s [=animator serialized options=] as options. + - The |serializedState| as state. + - The |destinationWorkletGlobalScope| as workletGlobalScope. + +
+ +If an animator state getter throws the user agent will remove the animator but does not recreate it. +This effectively removes the animator instance. + + +Requesting Animation Frames {#requesting-animation-frames} +---------------------------------------------------------- + +Each [=animator instance=] has an associated frame requested flag. It is initially set +to false. Different circumstances can cause the [=frame requested flag=] to be set to +true. These include the following: + - Changes in the [=current time=] of the animator's [=timeline=] + - Changes in the [=current time=] of the animator's corresponding [=worklet animation=] + +Performing [=run animators=] resets the [=frame requested flag=] on animators to false. + + +Web Animations Integration {#web-animation-integration} +======================================================= + + +Worklet Animation {#worklet-animation-desc} +------------------------------------------- +Worklet animation is a kind of [=animation=] that delegates animating its animation +effect to an [=animator instance=]. It controls the lifetime and playback state of its +[=corresponding animator instance=]. + +Being an [=animation=], [=worklet animation=] has an [=animation effect=] and a +[=timeline=]. However unlike other animations the worklet animation's [=current time=] does +not directly determine the animation effect's [=local time=] (via its [=inherited time=]). +Instead the associated [=animator instance=] controls the animation effect's [=local time=] +directly. Note that this means that the [=timeline's=] current time does not fully determine the +animation's output. + +[=Worklet animation=] has the following properties in addition to the {{Animation}} interface: + - : animation animator name + :: A string that identifies its [=animator definition=]. + - : serialized options + :: A serializable options object that is used whe constructing a new animator instance. + - : corresponding animator instance + :: A [=Animator Instance=]. + + +The existence of [=corresponding animator instance=] for a [=worklet animation=] depends on +the animation [=play state=]. See [[#web-animation-overrides]] for details on when and this +correspondence changes. + + +[Exposed=Window] +interface WorkletAnimation : Animation { + constructor(DOMString animatorName, + optional (AnimationEffect or sequence<AnimationEffect>)? effects = null, + optional AnimationTimeline? timeline, + optional any options); + readonly attribute DOMString animatorName; +}; + + + +
+ Overview of the WorkletAnimation timing model. +
+ Overview of the worklet animation timing model. +
+ The animation current time is input to the animator instance, which produces a local time value + for the animation effect. If the animator instance is running in a parallel global scope the + implementation may also choose to use the local time value to produce the animation output and + update the visuals in parallel. + +
+
+ + +Creating a Worklet Animation {#creating-worklet-animation} +---------------------------------------------------------- + +
+WorkletAnimation(|animatorName|, |effects|, |timeline|, |options|) + +Creates a new {{WorkletAnimation}} object using the following procedure: + + 1. Let |documentAnimatorDefinitions| be the associated [=document's=] document animator + definitions [=map=]. + + 2. If |documentAnimatorDefinitions|[|animatorName|] does not [=map/exists=], [=throw=] an + [=TypeError=] and abort the following steps. + + 3. If |documentAnimatorDefinitions|[|animatorName|] is "invalid", [=throw=] an + [=TypeError=] and abort the following steps. + + 4. Let |workletAnimation| be a new {{WorkletAnimation}} object. + + 5. Run the procedure to [=set the timeline of an animation=] on |workletAnimation| passing + |timeline| as the new timeline or, if a |timeline| argument is not provided, + passing the [=default document timeline=] of the {{Document}} associated with the + {{Window}} that is the [=current global object=]. + + 6. Let |effect| be the result corresponding to the first matching condition from below. + : If |effects| is a {{AnimationEffect}} object, + :: Let effect be |effects|. + : If |effects| is a [=list=] of {{AnimationEffect}} objects, + :: Let |effect| be a new {{WorkletGroupEffect}} with its children set to |effects|. + : Otherwise, + :: Let |effect| be undefined. + + 7. Let |serializedOptions| be the result of [=StructuredSerialize=](|options|). + Rethrow any exceptions. + + 8. Set the [=serialized options=] of |workletAnimation| to |serializedOptions|. + + 9. Set the [=animation animator name=] of |workletAnimation| to |animatorName|. + + 10. Run the procedure to [=set the target effect of an animation=] on |workletAnimation| + passing |effect| as the new effect. Note that this may trigger action to + [=set animator instance of worklet animation=]. See [[#web-animation-overrides]] for more + details. + +
+ + +Worklet Animation Timing and Sync Model {#timing-and-sync-model} +---------------------------------------------------------------- + +This section describes how [=worklet animation's=] timing model differs from other +[=animations=]. + +As described in [[#worklet-animation-desc]], the [=worklet animation's=] [=current time=] does +not determine its [=animation effect's=] [=local time=]. Instead the associated +[=animator instance=] controls the animation effect's [=local time=] directly. This means that the +animation effect's local time is controlled from a {{WorkletGlobalScope}} which may be in a parallel +execution context. + +Here are a few implications of the above semantics: + + - Setting the [=current time=] or [=start time=] of a [=worklet animation=] does not + necessarily change its output, but may change the animation [=play state=]. + - Similarly, invoking {{Animation/finish()}} or updating a [=worklet animation's=] playback + rate does not necessarily change its output, but may change the animation [=play state=] + - Querying the animation effect's local time using {{AnimationEffect/getComputedTiming()}} + may return stale information, in the case where the [=animator instance=] is running in a + parallel execution context. + + +If a Worklet Animation animation is executing in a parallel worklet execution context, the last +known state of its Animator Effects should be periodically synced back to the main javascript +execution context. The synchronization of [=effect values=] from the parallel worklet execution +context to the main javascript execution context must occur before +[=running the animation frame callbacks=] as part of the document lifecycle. + +Note that due to the asynchronous nature of this animation model a script running in the main +javascript execution context may see a stale value when reading a [=target property=] that is +being animated by a Worklet Animation, compared to the value currently being used to produce the +visual frame that is visible to the user. This is similar to the effect of asynchronous scrolling +when reading scroll offsets in the main javascript execution context. + + +
+ +To sync local times to document for a given |workletGlobalScope| the user agent +must perform the action that corresponds to the first matching condition from the +following: + + + : If the |workletGlobalScope| is not running in a parallel execution context + :: perform the following steps immediately: + + : If the |workletGlobalScope| is running in a parallel execution context + :: [=queue a task=] to run the following steps before running the animation frame + callbacks as part of the document lifecycle: + + 1. Iterate over all [=animator instance=]s in the animation worklet's global scope + [=animator instance set=]. For each such |animator| perform the following steps: + + 1. If |animator|'s [=sync requested flag=] is false skip the rest of the steps. + + 2. Let |animatorEffect| be |animator|'s [=effect=]. + + 3. Let |effect| be |animatorEffect|'s [=corresponding effect=]. + + 4. Set |effect|'s local time to |animatorEffect|'s local time. + + 5. Set |animator|'s [=sync requested flag=] to false. + +
+ +
+ +To sync animation timings to worklet for a given |workletAnimation| the user agent +must perform the following steps: + + 1. If |workletAnimation| does not have a [=corresponding animator instance=], abort the + following steps. + + 2. Let |animator| be |workletAnimation|'s [=corresponding animator instance=]. + + 2. Let |workletGlobalScope| be the {{AnimationWorkletGlobalScope}} associated with + |workletAnimation|. + + 3. : If the |workletGlobalScope| is not running in a parallel execution context + :: perform the following steps immediately. + + : If the |workletGlobalScope| is running in a parallel execution context + :: [=queue a task=] to run the following steps: + + 1. Set |animator|'s [=animator current time=] to |workletAnimation|'s [=current time=] + + 2. Let |animatorEffect| be |animator|'s [=effect=]. + + 3. Let |effect| be |animatorEffect|'s [=corresponding effect=]. + + 4. Set the following properties on |animatorEffect| to be the same as |effect|: + * [=start delay=], + * [=end delay=], + * [=fill mode=], + * [=iteration start=], + * [=iteration count=], + * [=iteration duration=], + * [=playback direction=], and + * [=timing function=]. + + +
+ + +Note: Notice that the local time is not synced from the document to animation worklet. + + +Issue(811): Come with appropriate mechanism's for [=animator instance=] to get notified when its + animation currentTime is changing e.g., via reverse(), finish() or playbackRate change. So that + it can react appropriately. + + +Web Animations Overrides {#web-animation-overrides} +--------------------------------------------------- + +In addition to the existing conditions on when the [=animation=] is considered [=ready=], a +[=worklet animation=] is only considered [=ready=] when the following condition is also true: + + - the user agent has completed any setup required to create the [=worklet animation's=] + [=corresponding animator instance=]. + +When a given worklet animation's [=play state=] changes from [=idle=] to [=finished=], +[=running=], or [=paused=], run the procedure to +[=associate animator instance of worklet animation=] given the worket animation as +|workletAnimation|. + +When a given worklet animation's [=play state=] changes from [=finished=], [=running=] or +[=paused=] to [=idle=], run the procedure to +[=disassociate animator instance of worklet animation=]given the worklet animation as +|workletAnimation|. + +When a given worklet animation's [=replace state=] changes from [=active=] to either +[=persisted=] or [=removed=] run the procedure to +[=disassociate animator instance of worklet animation]= given the worklet animation as +|workletAnimation|. + + +Issue: In web-animation play state is updated before the actual change in particular some operations +such as play() are asynchronous. We should really invoke these Animator related operation after the +appropriate animation operation is complete instead of when play state has changed. This will +require either finding (or introducing) q new hook in web animation or having override for each such +async operation. + + +When the procedure to [=set the target effect of an animation=] for a given worklet animation +is called, then [=set animator instance of worklet animation=] given the worklet animation as +|workletAnimation|. + +When the procedure to [=set the timeline of an animation=] for a given |workletAnimation| +is called, then [=set animator instance of worklet animation=] given the worklet animation as +|workletAnimation|. + +When the procedure to [=set the current time=] or [=set the start time=] for a given worklet +animation is called, then [=sync animation timings to worklet=] given the worklet animation as +|workletAnimation|. + +When the procedure to [=update the timing properties of an animation effect=] for a given effect is +called and that effect is owned be a worklet animation, then +[=sync animation timings to worklet=] given that worklet animation as |workletAnimation|. + + +
+ +To associate animator instance of worklet animation given |workletAnimation|, +the user agent must run the following steps: + + 1. If |workletAnimation| has a [=corresponding animator instance=], abort the following steps. + 2. Let |workletGlobalScope| be the {{AnimationWorkletGlobalScope}} associated with + |workletAnimation|. + 3. [=Queue a task=] on |workletGlobalScope| to run the procedure to create a new animator + instance, passing: + * The |workletAnimation|'s [=animation animator name=] as name. + * The |workletAnimation|'s [=timeline=] as timeline. + * The |workletAnimation|'s [=animation effect=] as effect. + * The |workletAnimation|'s [=serialized options=] as options. + * The |workletGlobalScope| as workletGlobalScope. + 4. If the procedure was successful, set the resulting [=animator instance=] to be the + [=corresponding animator instance=] of |workletAnimation|. + +
+ +
+ +To disassociate animator instance of worklet animation given +|workletAnimation|, the user age must run the following steps: + + 1. If |workletAnimation| does not have a [=corresponding animator instance=], abort the + following steps. + 2. Let |workletGlobalScope| be the {{AnimationWorkletGlobalScope}} associated with + |workletAnimation|. + 3. Let |animatorInstance| be |workletAnimation|'s [=corresponding animator instance=]. + 4. [=Queue a task=] on the |workletGlobalScope| to run the procedure to remove an animator + instance, passing |animatorInstance| as instance and |workletGlobalScope| as + workletGlobalScope. + 5. Set |workletAnimation|'s [=corresponding animator instance=] as undefined. + +
+ +
+ +To set animator instance of worklet animation given +|workletAnimation|, the user agent must run the following steps: + + 1. [=disassociate animator instance of worklet animation=] given |workletAnimation|. + 2. [=associate animator instance of worklet animation=] given |workletAnimation|. + +
+ +Effect Stack and Composite Order {#effect-stack-composite-order} +---------------------------------------------------------------- + +As with other animations, [=worklet animations=] participate in the [=effect stack=]. A worklet +animation does not have a specific [=animation class=] which means it has the same composite order +as other Javascript created web animations. + + + + + +Additional Related Concepts {#related-concepts} +=============================================== + + +Worklet Group Effect {#worklet-group-effect} +-------------------------------------------- +This section is not normative. + + +[=Group effect=] is a type of [=animation effect=] that enbales multiple child animation +effects to be animated together as a group. + +{{WorkletGroupEffect}} is a type of [=group effect=] that allows its [=child effect's=] +[=local times=] to be mutated individually. + +When a {{WorkletGroupEffect}} is set as the [=animation effect=] of a [=worklet animation=], +its [=corresponding animator instance=] can directly control the [=child effects=]' local +times. This allows a single worklet animation to coordinate multiple effects - see +[[#example-2]] for an example of such a use-case. + + +[Exposed=AnimationWorklet] +interface WorkletGroupEffect { + sequence<WorkletAnimationEffect> getChildren(); +}; + + + +Issue(w3c/csswg-drafts#2071): The above interface exposes a conservative subset of GroupEffect +proposed as part of web-animation-2. We should instead move this into a delta spec against the +web-animation. + + +Note: Group Effect is currently an experimental feature and not well specified in web animations. So +the concept of {{WorkletGroupEffect}} may change dramatically as [=Group Effect=] get specified. +See https://github.com/yi-gu/group_effect/blob/master/README.md for more details. + + +ScrollTimeline {#scroll-timeline} +--------------------------------- +This section is not normative. + + +{{ScrollTimeline}} is a new concept being proposed for addition to web animation API. It defines +an animation timeline whose time value depends on the scroll position of a scroll container. +[=Worklet animations=] can have a scroll timeline and thus drive their scripted effects based +on a scroll offset. + +Note: Access to input: We are interested on exposing additional user input beside +scrolling (e.g., touch/pointer input) to these animations so that authors can create jank-free +input driven animations which are not really possible today. We are still trying to figure out the +right abstractions and mechanisms to do this. + + + +Security Considerations {#security-considerations} +================================================== + +There are no known security issues introduced by these features. + +Privacy Considerations {#privacy-considerations} +================================================ + +There are no known privacy issues introduced by these features. + +Examples {#examples} +==================== + + +Example 1: Spring timing. {#example-1} +--------------------------------------- +Here we use Animation Worklet to create animation with a custom spring timing. + + + + +<div id='target'></div> + +<script> +await CSS.animationWorklet.addModule('spring-animator.js'); +targetEl = document.getElementById('target'); + +const effect = new KeyframeEffect( + targetEl, + {transform: ['translateX(0)', 'translateX(50vw)']}, + {duration: 1000} +); +const animation = new WorkletAnimation('spring', effect, document.timeline, {k: 2, ratio: 0.7}); +animation.play(); +</script> + + + + + +registerAnimator('spring', class SpringAnimator { + constructor(options = {k: 1, ratio: 0.5}) { + this.timing = createSpring(options.k, options.ratio); + } + + animate(currentTime, effect) { + let delta = this.timing(currentTime); + // scale this by target duration + delta = delta * (effect.getTimings().duration / 2); + effect.localTime = delta; + // TODO: Provide a method for animate to mark animation as finished once + // spring simulation is complete, e.g., this.finish() + // See issue https://github.com/w3c/css-houdini-drafts/issues/808 + } +}); + +function createSpring(springConstant, ratio) { + // Normalize mass and distance to 1 and assume a reasonable init velocit + // but these can also become options to this animator. + const velocity = 0.2; + const mass = 1; + const distance = 1; + + // Keep ratio < 1 to ensure it is under-damped. + ratio = Math.min(ratio, 1 - 1e-5); + + const damping = ratio * 2.0 * Math.sqrt(springConstant); + const w = Math.sqrt(4.0 * springConstant - damping * damping) / (2.0 * mass); + const r = -(damping / 2.0); + const c1 = distance; + const c2 = (velocity - r * distance) / w; + + // return a value in [0..distance] + return function springTiming(timeMs) { + const time = timeMs / 1000; // in seconds + const result = Math.pow(Math.E, r * time) * + (c1 * Math.cos(w * time) + c2 * Math.sin(w * time)); + return distance - result; + } +} + + + +Example 2: Twitter header. {#example-2} +--------------------------------------- +An example of twitter profile header effect where two elements (avatar, and header) are updated in +sync with scroll offset with an additional feature where avatar can have additional physic based +movement based on the velocity and acceleration of the scrolling. + + + +<div id='scrollingContainer'> + <div id='header' style='height: 150px'></div> + <div id='avatar'><img></div> +</div> + +// In document scope. +<script> +const headerEl = document.getElementById('header'); +const avatarEl = document.getElementById('avatar'); +const scrollingContainerEl = document.getElementById('scrollingContainer'); + + +const scrollTimeline = new ScrollTimeline({ + scrollSource: scrollingContainerEl, + orientation: 'block', + timeRange: 1000, + startScrollOffset: 0, + endScrollOffset: headerEl.clientHeight +}); + +const effects = [ + /* avatar scales down as we scroll up */ + new KeyframeEffect(avatarEl, + {transform: ['scale(1)', 'scale(0.5)']}, + {duration: scrollTimeline.timeRange}), + /* header loses transparency as we scroll up */ + new KeyframeEffect(headerEl, + {opacity: [0, 0.8]}, + {duration: scrollTimeline.timeRange}) +]; + +await CSS.animationWorklet.addModule('twitter-header-animator.js'); +const animation = new WorkletAnimation('twitter-header', effects, scrollTimeline); + +animation.play(); +</script> + + + + +// Inside AnimationWorkletGlobalScope. +registerAnimator('twitter-header', class HeaderAnimator { + constructor(options, state = {velocity: 0, acceleration: 0}) { + // `state` is either undefined (first time) or it is the previous state (after an animator + // is migrated between global scopes). + this.velocity = state.velocity; + this.acceleration = state.acceleration; + } + + + animate(currentTime, effect) { + const scroll = currentTime; // scroll is in [0, 1000] range + + if (this.prevScroll) { + this.velocity = scroll - this.prevScroll; + this.acceleration = this.velocity - this.prevVelocity; + } + this.prevScroll = scroll; + this.prevVelocity = velocity; + + + // Drive the output group effect by setting its children local times individually. + effect.children[0].localTime = scroll; + + effect.children[1].localTime = curve(velocity, acceleration, scroll); + } + + state() { + // Invoked before any migration attempts. The returned object must be structure clonable + // and will be passed to constructor to help animator restore its state after migration to the + // new scope. + return { + this.velocity, + this.acceleration + } + } + +}); + +curve(scroll, velocity, acceleration) { + + return /* compute an return a physical movement curve based on scroll position, and per frame + velocity and acceleration. */ ; +} + + + +Example 3: Parallax backgrounds. {#example-3} +--------------------------------------------- +A simple parallax background example. + + +<style> +.parallax { + position: fixed; + top: 0; + left: 0; + opacity: 0.5; +} +</style> +<div id='scrollingContainer'> + <div id="slow" class="parallax"></div> + <div id="fast" class="parallax"></div> +</div> + +<script> +await CSS.animationWorklet.addModule('parallax-animator.js'); + +const parallaxSlowEl = document.getElementById('slow'); +const parallaxFastEl = document.getElementById('fast'); +const scrollingContainerEl = document.getElementById('scrollingContainer'); + +const scrollTimeline = new ScrollTimeline({ + scrollSource: scrollingContainerEl, + orientation: 'block', + timeRange: 1000 +}); +const scrollRange = scrollingContainerEl.scrollHeight - scrollingContainerEl.clientHeight; + +const slowParallax = new WorkletAnimation( + 'parallax', + new KeyframeEffect(parallaxSlowEl, + {'transform': ['translateY(0)', 'translateY(' + -scrollRange + 'px)']}, + {duration: scrollTimeline.timeRange}), + scrollTimeline, + {rate : 0.4} +); +slowParallax.play(); + +const fastParallax = new WorkletAnimation( + 'parallax', + new KeyframeEffect(parallaxFastEl, + {'transform': ['translateY(0)', 'translateY(' + -scrollRange + 'px)']}, + {duration: scrollTimeline.timeRange}), + scrollTimeline, + {rate : 0.8} +); +fastParallax.play(); +</script> + + + + +// Inside AnimationWorkletGlobalScope. +registerAnimator('parallax', class ParallaxAnimator { + constructor(options) { + this.rate_ = options.rate; + } + + animate(currentTime, effect) { + effect.localTime = currentTime * this.rate_; + } +}); + diff --git a/css-animation-worklet-1/README.md b/css-animation-worklet-1/README.md new file mode 100644 index 00000000..f88e06b9 --- /dev/null +++ b/css-animation-worklet-1/README.md @@ -0,0 +1,665 @@ +# Animation Worklet Explainer +--- + +# Overview + +Animation Worklet is a new primitive that provides extensibility in web animations and enables high +performance procedural animations on the web. The feature is developed as part of the +[CSS Houdini task force](https://github.com/w3c/css-houdini-drafts/wiki). + +The Animation Worklet API provides a method to create scripted animations that control a set of +animation effects. These animations are executed inside an isolated execution environment, *worklet* +which makes it possible for user agents to run such animations in their own dedicated thread to +provide a degree of performance isolation from main thread. The API is compatible with Web +Animations and uses existing constructs as much as possible. + +# Background + +Scripted interactive effects (written in response to `requestAnimationFrame`, `pointer events` or +async `onscroll` events) are rich but are subject to main thread jankiness. On the other hand, +accelerated CSS transitions and animations can be fast (for a subset of *accelerated* properties) +but are not rich enough to enable [many common use cases](#motivating-use-cases) and currently have +no way to access key user input (pointer events, gestures, scroll). This is why scripted effects are +still very popular for implementing common effects such as hidey-bars, parallax, pull-to-refresh, +drag-and-drop, swipe to dismiss and etc. Animation Worklet provides is key building block for +enabling creation of smooth rich interactive visual effects on the web while also exposing an +extensibility hook in web animations. + + +See the [Animation Worklet design principles and goals](principles.md) for a more extended overview +of the motivations behind Animation Worklet and how the design will be evolved to support a growing +set of use cases. Also see [the status document](status.md) for high level implementation status and +timeline. [Here][roc-thread] you may find an earlier high level discussion on general approaches to +address this problem. + + +# Motivating Use Cases + +* Scroll driven effects: + * Hidey-bar ([demo](https://googlechromelabs.github.io/houdini-samples/animation-worklet/twitter-header/)): animation depends on both scroll and time input. + * Parallax ([demo](https://googlechromelabs.github.io/houdini-samples/animation-worklet/parallax-scrolling/)): simplest scroll-driven effect. + * Custom paginated slider ([demo](http://aw-playground.glitch.me/amp-scroller.html)). + * Pull-to-refresh: animation depends on both touch and time inputs. + * Custom scrollbars. + * High-fidelity location tracking and positioning + * [More examples](https://github.com/w3c/css-houdini-drafts/blob/master/scroll-customization-api/UseCases.md) of scroll-driven effects. +* Gesture driven effects: + * [Image manipulator](https://github.com/w3c/csswg-drafts/issues/2493#issuecomment-422153926) that scales, rotates etc. + * Swipe to Action. + * Drag-N-Drop. + * Tiled panning e.g., Google maps. +* Stateful script driven effects: + * Spring-Sticky effect ([demo](http://googlechromelabs.github.io/houdini-samples/animation-worklet/spring-sticky/)). + * Touch-driven physical environments. + * Expando ([demo](http://googlechromelabs.github.io/houdini-samples/animation-worklet/expando/)): Procedural animations with multiple elements. +* Animated scroll offsets: + * Having multiple scrollers scroll in sync e.g. diff viewer keeping old/new in sync when you + scroll either ([demo](https://googlechromelabs.github.io/houdini-samples/animation-worklet/sync-scroller/)) + * Custom smooth scroll animations (e.g., physic based fling curves) +* Animation Extensibility: + * Custom animation timings (particularly those that are not calculable a priori e.g., [spring demo](https://googlechromelabs.github.io/houdini-samples/animation-worklet/spring-timing/)) + * Custom animation sequencing which involves complex coordination across multiple effects. + + +Not all of these usecases are immediately enabled by the current proposed API. However Animation +Worklet provides a powerfull primitive (off main-thread scripted animation) which when combined with +other upcoming features (e.g., +[Event in Worklets](https://github.com/w3c/css-houdini-drafts/issues/834), +[ScrollTimeline](https://wicg.github.io/scroll-animations/), +[GroupEffect](https://github.com/w3c/csswg-drafts/issues/2071)) can address all these usecases and +allows many of currently main-thread rAF-based animations to move off thread with significant +improvement to their smoothness. +See [Animation Worklet design principles and goals](principles.md) for a more extended discussion +of this. + + +***Note***: Demos work best in the latest Chrome Canary with the experimental +web platform features enabled (`--enable-experimental-web-platform-features` +flag) otherwise they fallback to using main thread rAF to emulate the behaviour. + + +# Animation Worklet + +Animation Worklet attempts to address the above usecases by introducing a new primitive that enables +extensibility in the web's core animation model [WebAnimations][WA]): custom frame-by-frame animate +function! + + +## How It Works + +Normally, an active animation takes its timeline time and according to its running state (e.g., +playing, finished) and playback rate, computes its own **current time** which it then uses to set +its keyframe effect **local time**. Here is a simple example of a simple animation: + +```js +const effect = new KeyframeEffect(targetEl, + {transform: ['translateX(0)', 'translateX(50vw)']}, + {duration: 1000} +); +const animation = new Animation(effect, document.timeline); +animation.play(); +``` + + +Animation Worklet allows this transformation from **current time** to **local time** to be +customized via a special Javascript function `animate`. Similar to other Houdini worklets, these +animate functions are called inside a restricted [worklet][worklet] context (`AnimationWorkletGlobalScope`) +which means the don't have access to main document. Another implication is that implementor can run +these off-thread to ensure smooth animations even when main thread is busy which is a key +performance goal for animations. + +To leverage this machinery, web developer creates a special Animation subclass, `WorkletAnimation`. +The only difference is that the WorkletAnimation constructor takes a `name` argument that identifies +the custom animate function to be used. Animation Worklet then creates a corresponding *animater* +instance that represent this particlar animation and then on each animation frame calls its +`animate` function to determine the local time which ultimately drives the keyframe effect. + + +![Overview of the WorkletAnimation Timing Model](img/WorkletAnimation-timing-model.svg) + +Here the same simple example but using Animation Worklet instead. + +**index.html** +```js +// Load your custom animator in the worklet +await CSS.animationWorklet.addModule('animator.js'); + +const effect = new KeyframeEffect(targetEl, + {transform: ['translateX(0)', 'translateX(50vw)']}, + {duration: 1000} +); +const animation = new WorkletAnimation('my-awesome-animator', effect); +animation.play(); +``` + +**animator.js** +``` +registerAnimator('my-awesome-animator', class Passthrough extends StatelessAnimator { + animate(currentTime, effect) { + // The simplest custom animator that does exactly what regular animations do! + effect.localTime = currentTime; + } +}); +``` + + +A few notable things: + + - WorkletAnimation behaves the same as regular animations e.g., it can be played/paused/canceled + - WorkletAnimation can optionally accept an options bag to help the corresponding Animator + configure itself during construction. + - Animator controls the output of the animation by setting the AnimationEffect.localTime + - There is two types of Animators: Stateless and Statefull explicitly marked using superclasses. + +Below are a few more complex example each trying to show a different aspect of Animation Worklet. + +# Examples + +## Spring Timing + +Here we use Animation Worklet to create animation with a custom spring timing. + + +```html + +
+ + +``` + +spring-animator.js: + +```js +registerAnimator('spring', class SpringAnimator extends StatelessAnimator { + constructor(options = {k: 1, ratio: 0.5}) { + this.timing = createSpring(options.k, options.ratio); + } + + animate(currentTime, effect) { + let delta = this.timing(currentTime); + // scale this by target duration + delta = delta * (effect.getTimings().duration / 2); + effect.localTime = delta; + // TODO: Provide a method for animate to mark animation as finished once + // spring simulation is complete, e.g., this.finish() + // See issue https://github.com/w3c/css-houdini-drafts/issues/808 + } +}); + +function createSpring(springConstant, ratio) { + // Normalize mass and distance to 1 and assume a reasonable init velocit + // but these can also become options to this animator. + const velocity = 0.2; + const mass = 1; + const distance = 1; + + // Keep ratio < 1 to ensure it is under-damped. + ratio = Math.min(ratio, 1 - 1e-5); + + const damping = ratio * 2.0 * Math.sqrt(springConstant); + const w = Math.sqrt(4.0 * springConstant - damping * damping) / (2.0 * mass); + const r = -(damping / 2.0); + const c1 = distance; + const c2 = (velocity - r * distance) / w; + + // return a value in [0..distance] + return function springTiming(timeMs) { + const time = timeMs / 1000; // in seconds + const result = Math.pow(Math.E, r * time) * + (c1 * Math.cos(w * time) + c2 * Math.sin(w * time)); + return distance - result; + } +} +``` + +Note that ideally once sping simulation is finished, the worklet animation would also dispatch +the `finish` event. Adding the necessary mechanism to enable this is tracked +[here](https://github.com/w3c/css-houdini-drafts/issues/808). + +## Twitter Header + +Note: This assumes experimental [ScrollTimeline][scroll-timeline] feature. + +An example of twitter profile header effect where two elements (avatar, and header) are updated in +sync with scroll offset. + +```html + +
+ +
+
+ + +``` + +twitter-header-animator.js: +```js +registerAnimator('twitter-header', class TwitterHeader extends StatelessAnimator { + constructor(options) { + this.timing_ = new CubicBezier('ease-out'); + } + + clamp(value, min, max) { + return Math.min(Math.max(value, min), max); + } + + animate(currentTime, effect) { + const scroll = currentTime; // [0, 1] + + // Drive the output group effect by setting its children local times. + effect.children[0].localTime = scroll; + // Can control the child effects individually +    effect.children[1].localTime = this.timing_(this.clamp(scroll, 0, 1)); + } +}); +``` + +## Swipe-to-Action + +Another usecase for Animation Worklet is to enable interactive input-driven animation effects that +are driven both by input events and time. + +To enable this we need a way to receive pointer events in worklet (e.g. via [CSS custom +variables](https://github.com/w3c/css-houdini-drafts/issues/869) or [other +mechanisms][input-for-worker]) and +also allow [playback controls](https://github.com/w3c/css-houdini-drafts/issues/808) inside +worklets. Both of these are natural planned additions to Animation Worklet. + + +Consider a simple swipe-to-action effect which follows the user swipe gesture and when finger lifts +then continues to completion (either dismissed or returned to origin) with a curve that matches the +swipe gesture's velocity. (See this [example](https://twitter.com/kzzzf/status/917444054887124992)) + +With Animation Worklet, this can be modeled as a stateful animator which consumes both time and +pointer events and have the following state machines: + +![SwipeToCompletionAnimation](img/swipe-to-dismiss-state.png) + + +Here are the three main states: + +1. Animation is idle, where it is `paused` so that it is not actively ticking +2. As soon as the user touches down, the animation moves the target to follow the user touchpoint + while staying `paused` (optionally calculate the movement velocity, and overall delta). +3. As soon as the user lift their finger the animation will the switch to 'playing' so that it is + ticked by time until it reaches its finished state. The final state may be decided on overall + delta and velocity and the animation curve adapts to the movement velocity. + +Note that while in (3), if the user touches down we go back to (2) which ensures responsiveness to +user touch input. + +To make this more concrete, here is how this may be implemented (assuming strawman proposed APIs for +playback controls and also receiving pointer events). Note that all the state machine transitions +and various state data (velocity, phase) and internal to the animator. Main thread only needs to +provide appropriate keyframes that can used to translate the element on the viewport as appropriate +(e.g., `Keyframes(target, {transform: ['translateX(-100vw)', 'translateX(100vw)']})`). + + +```javascript +registerAnimator('swipe-to-dismiss', class SwipeAnimator extends StatefulAnimator { + constructor(options, state = {velocity: 0, phase: 'idle'}) { + this.velocity = state.velocity; + this.phase = state.phase; + + if (phase == 'idle') { + // Pause until we receive pointer events. + this.pause(); + } + + // Assumes we have an API to receive pointer events for our target. + this.addEventListener("eventtargetadded", (event) => { + for (type of ["pointerdown", "pointermove", "pointerup"]) { + event.target.addEventListener(type,onPointerEvent ); + } + }); + } + + onpointerevent(event) { + if (event.type == "pointerdown" || event.type == "pointermove") { + this.phase = "follow_pointer"; + } else { + this.phase = "animate_to_completion"; + // Also decide what is the completion phase (e.g., hide or show) + } + + this.pointer_position = event.screenX; + + // Allow the animation to play for *one* frame to react to the pointer event. + this.play(); + } + + animate(currentTime, effect) { + if (this.phase == "follow_pointer") { + effect.localTime = position_curve(this.pointer_position); + update_velocity(currentTime, this.pointer_position); + // Pause, no need to produce frames until next pointer event. + this.pause(); + } else if (this.phase = "animate_to_completion") { + effect.localTime = time_curve(currentTime, velocity); + + if (effect.localTime == 0 || effect.localTime == effect.duration) { + // The animation is complete. Pause and become idle until next user interaction. + this.phase = "idle"; + this.pause(); + } else { + // Continue producing frames based on time until we complete or the user interacts again. + this.play(); + } + } + + } + + position_curve(x) { + // map finger position to local time so we follow user's touch. + } + + time_curve(time, velocity) { + // Map current time delta and given movement velocity to appropriate local time so that over + // time we animate to a final position. + } + + update_velocity(time, x) { + this.velocity = (x - last_x) / (time - last_time); + this.last_time = time; + this.last_x = x; + } + + state() { + return { + phase: this.phase, + velocity: this.velocity + } + } +}); +``` + +```javascript + +await CSS.animationWorklet.addModule('swipe-to-dismiss-animator.js'); +const target = document.getElementById('target'); +const s2d = new WorkletAnimation( + 'swipe-to-dismiss', + new KeyframeEffect(target, {transform: ['translateX(-100vw)', 'translateX(100vw)']})); +s2d.play(); +``` + + +# Why Extend Animation? + +In [WebAnimation][WA], [Animation][animation] is the main controller. It handles the playback commands +(play/pause/cancel) and is responsible for processing the progressing time (sourced from Timeline) and +driving keyframes effect which defines how a particular target css property is animated and +ultimately pixels moving on the screen. + +By allowing extensibility in Animation we can have the most flexibility in terms of what is possible +for example animation can directly control the following: + - Control animation playback e.g., implement open-ended animations with non-deterministic timings + (e.g., physical-based) or provide "trigger" facilities + - Flexibility in transforming other forms of input into "time" e.g., consume touch events and drive + animations + - Ability to handle multiple timelines e.g., animations that seamlessly transition btween being + touch/scroll driven to time-driven + - Control how time is translated e.g., new custom easing functions + - Drive multiple effects and control how they relate to each other e.g., new effect sequencing + + +While there is benefit in extensibility in other parts of animation stack (custom timeline, custom +effect, custom timing), custom animations provides the largest value in terms of flexibility and +addressing key usecases so it is the one we are tackling first. + +Animation Worklet can be easily augmented in future to support other Houdini style extensibility +features as well. + + +TODO: Also discuss other models that we have considered (e.g., CompositorWorker) that bypassed +web animation altogether. + + + +# Key Concepts + +## Animation Worklet Global Scope +A [worklet global scope](https://drafts.css-houdini.org/worklets/#the-global-scope) that is created +by Animation Worklet. Note that Animation Worklet creates multiple such scopes and uses them to +execute user defined effects. In particular global scopes are regularly switched to enforce +stateless and stateful animator contracts. + + +## Animator + +Animator is a Javascript class that encapsulates the custom animation logic. Similar to other +Houdinig worklets, animators are registered inside the worklet global scope with a unique name which +can be used to uniquely identify them. + + +## WorkletAnimation +`WorkletAnimation` is a subclass of Animation that can be used to create an custom animation that +runs inside a standalone animation worklet scope. A worklet animation has a corresponding animator +instance in a animation worklet scope which is responsible to drive its keyframe effects. Here are +the key differences compared to a regular web animation: + - Name: The name identifies the custom animator class registered in the animation worklet scope. + - Options: `WorkletAnimation` may have a custom properties bag that is cloned and provided to the + corresponding animator constructor when it is being instantiated. + +Note that worklet animations expose same API surface as other web animations and thus they may be +created, played, paused, inspected, and generally controlled from the main document scope. Here is +how various methods roughly translate: + + - `cancel()`: cancels the animation and the corresponding animator instance is removed. + - `play()`: starts the animation and the corresponding animator instance gets constructed and + may get its `animate` function called periodically as a result of changes in its timelines. + - pause(): pauses the animation and the corresponding animator instance no longer receives + `animate` calls. + - finish(), reverse() or mutating playbackRate: these affect the currentTime which is seens by + the animator instance. (We are considering possiblity of having a `onPlaybackRateChanged` + callback) + +## Statefull and Statelss Animators + +Sometimes animation effects require maintaining internal state (e.g., when animation needs to depend +on velocity). Such animators have to explicitly declare their statefulness but by inheritting from +`StatefulAnimator` superclass. + +The animators are not guaranteed to run in the same global scope (or underlying thread) for their +lifetime duration. For example user agents are free to initially run the animator on main thread +but later decide to migrate it off main thread to get certain performance optimizations or to tear +down scopes to save resources. + +Animation Worklet helps stateful animators to maintain their state across such migration events. +This is done through a state() function which is called and animator exposes its state. Here is +an example: + +```js +// in document scope +new WorkletAnimation('animation-with-local-state', keyframes); +``` + +```js +registerAnimator('animation-with-local-state', class FoorAnimator extends StatefulAnimator { + constructor(options, state = {velocity: 0, acceleration: 0}) { + // state is either undefined (first time) or the state after an animator is migrated across + // global scope. + this.velocity = state.velocity; + this.acceleration = state.acceleration; + } + + animate(time, effect) { + if (this.lastTime) { + this.velocity = time - this.prevTime; + this.acceleration = this.velocity - this.prevVelocity; + } + this.prevTime = time; + this.prevVelocity = velocity; + + effect.localTime = curve(velocity, acceleration, currentTime); + } + + state() { + // Invoked before any migration attempts. The returned object must be structure clonable + // and will be passed to constructor to help animator restore its state after migration to the + // new scope. + return { + this.velocity, + this.acceleration + } + } + + curve(velocity, accerlation, t) { + return /* compute some physical movement curve */; + } +}); +``` + + +## Threading Model + +Animation Worklet is designed to be thread-agnostic. Rendering engines may create one or more +parallel worklet execution contexts separate from the main javascript execution context, e.g., on +their own dedicated threads. Rendering engines may then choose to assign Animation Worklet +animations to run in such contexts. Doing so allows Animation Worklet animations to avoid being +impacted by main thread jank. + +Rendering engines may wish to make a best-effort attempt to execute animate callbacks synchronously +with visual frame production to ensure smooth animation. However it is legal for rendering engines +to produce visual frames without blocking to receive animation updates from a worklet (i.e., letting +the effects slip behind). For example, this could occur when the animate function callback is +unable to complete before the frame deadline. + +We believe that scripted animations which are run in a parallel execution environment and which +limit themselves to animating properties which do not require the user agent to consult main thread +will have a much better chance of meeting the strict frame budgets required for smooth playback. + + +Note that due to the asynchronous nature of this animation model a script running in the main +javascript execution context may see a stale value when reading a target property that is +being animated in a Worklet Animation, compared to the value currently being used to produce the +visual frame that is visible to the user. This is similar to the effect of asynchronous scrolling +when reading scroll offsets in the main javascript execution context. + + +
+ Overview of the animation worklet threading model. +
+ Overview of the animation worklet threading model.
+ + A simplified visualization of how animators running in a parallel execution environment can sync + their update to main thread while remaining in sync with visual frame production. +
+
+ + +# Related Concepts + +The following concepts are not part of Animation Worklet specification but Animation Worklet is +designed to take advantage of them to enable a richer set of usecases. These are still in early +stages of the standardization process so their API may change over time. + +## ScrollTimeline +[ScrollTimeline][scroll-timeline] is a concept introduced in +scroll-linked animation proposal. It defines an animation timeline whose time value depends on +scroll position of a scroll container. `ScrollTimeline` can be used an an input timeline for +worklet animations and it is the intended mechanisms to give read access to scroll position. + +We can later add additional properties to this timeline (e.g., scroll phase (active, inertial, +overscroll), velocity, direction) that can further be used by Animation Worklet. + +## GroupEffect + +[GroupEffect][group-effect] is a concept introduced in Web Animation Level 2 specification. It +provides a way to group multiple effects in a tree structure. `GroupEffect` can be used as the +output for Worklet Animations making it possible for it to drive complext effects spanning multiple +elements. Also with some minor [proposed changes](group-effect-changes) to Group Effect timing +model, Animation Worklet can enable creation of new custom sequencing models (e.g., with conditions +and state). + +## Event Dispatching to Worker and Worklets +[Event Dispatching to Worker/Worklets][input-for-worker] is a proposal in WICG which allows workers +and worklets to passively receive DOM events and in particular Pointer Events. This can be +benefitial to Animation Worklet as it provides an ergonomic and low latency way for Animation +Worklet to receive pointer events thus enabling it to implement input driven animations more +effectively. + + +# WEBIDL + +`WorkletAnimation` extends `Animation` and adds a getter for its timelines. +Its constructor takes: + - `animatiorId` which should match the id of an animator which is registered in +the animation worklet scope. + - A sequence of effects which are passed into a `GroupEffect` constructor. + - A sequence of timelines, the first one of which is considered primary timeline and passed to + `Animation` constructor. + + +```webidl + +[Constructor (DOMString animatorName, + optional (AnimationEffectReadOnly or array)? effects = null, + AnimationTimeline? timeline, + optional WorkletAnimationOptions)] +interface WorkletAnimation : Animation { + readonly attribute DOMString animatorName; +} +``` + +`AnimationEffectReadOnly` gets a writable `localTime` attribute which may be used to drive the +effect from the worklet global scope. + +```webidl +partial interface AnimationEffectReadOnly { + [Exposed=Worklet] + // Intended for use inside Animation Worklet scope to drive the effect. + attribute double localTime; +}; + +``` + +# Specification +The [draft specification](https://drafts.css-houdini.org/css-animationworklet) is +the most recent version. + + +[roc-thread]: https://lists.w3.org/Archives/Public/public-houdini/2015Mar/0020.html +[cw-proposal]: https://github.com/w3c/css-houdini-drafts/blob/master/composited-scrolling-and-animation/Explainer.md +[WA]: https://drafts.csswg.org/web-animations/ +[animation]: https://drafts.csswg.org/web-animations/#animations +[worklet]: https://drafts.css-houdini.org/worklets/#worklet-section +[input-for-worker]: https://discourse.wicg.io/t/proposal-exposing-input-events-to-worker-threads/3479 +[group-effect]: https://w3c.github.io/web-animations/level-2/#the-animationgroup-interfaces +[group-effect-changes]: https://github.com/yi-gu/group_effects +[scroll-timeline]: https://wicg.github.io/scroll-animations/#scrolltimeline \ No newline at end of file diff --git a/css-animation-worklet-1/WIP.md b/css-animation-worklet-1/WIP.md new file mode 100644 index 00000000..9b36c15a --- /dev/null +++ b/css-animation-worklet-1/WIP.md @@ -0,0 +1,103 @@ +# Open API Questions +--- + + +## Creation/Registration timing + +What should happen if an animation is created before the animator is registered? + +## Timelines + +* observe-only timelines? i.e., have access to timeline but the animate is not triggered when its + value changes. + +* Should we have a Timeline.currentTime and Timeline.localTime, where the latter is + the former but offset by startTime & scaled by playbackRate? + +* Access to the actual scroll position in the ScrollTimeline + +* Access to scroll phase (inactive, active, inertial etc.) + + +## Updating Elements + +For some effects we need to be able to add new participating elements without +restarting the effect. Here is an initial idea on how this can work. + +```js +// Effects and data can change after some time. +// We might want to break this out into separate functions or optional +// updates so you can just update options or just effects and options +// without having to pass other parameters again. +anim.update({ + [ /* new list of effects? */], + [ /* new list of timelines */], + {/* options */} +}); +``` + +```js +// In worklet scope +class MyAnimator{ + update(options) { + // this is a V2 concept, + } +} +``` + +## CSS Notation + +We are not proposing including this in the initial spec, but including some +preliminary thoughts here so that we can keep the eventual declarative CSS +specification in mind. + +index.html: +```html +<-- animator instance is declared here, with its timelines --> +
+ <-- effect timing is declared here and assigned to above animator --> +
+
+
+
+``` + +style.css: +```css +#main { + animation: worklet('twitter-header') + animation-timeline: scroll(#scroller_element.....) /* https://wicg.github.io/scroll-animations/#animation-timeline */ +} + +/* These are descendants of the animation */ +#main .header { + animation-group: 'twitter-header' 'header' #... + /* This syntax should be similar to what the plan is for Web Animation Group Effects */ +} + +#main .avatar { + animation-group: 'twitter-header' 'avatar' #... +} +``` + +This is equivalent to calling: + +```js +new WorkletAnimation('twitter-header', + [ + new KeyFrameEffect(.header[0], [], {}), + new KeyFrameEffect(.avatar, [], {}), + new KeyFrameEffect(.header[1], [], {}), + ], + [new ScrollingTimeline(#selector, {...})], + { elements: [ + /* This is admittedly a bit magical. */ + {'name': 'header'}, + {'name': 'avatar'}, + {'name': 'header'}, + ]} +).play(); + +``` + +Adding new elements that match the selector will be equivalent to invoking `update`. diff --git a/css-animation-worklet-1/img/AnimationWorklet-threading-model.svg b/css-animation-worklet-1/img/AnimationWorklet-threading-model.svg new file mode 100644 index 00000000..d463680b --- /dev/null +++ b/css-animation-worklet-1/img/AnimationWorklet-threading-model.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css-animation-worklet-1/img/WorkletAnimation-timing-model.svg b/css-animation-worklet-1/img/WorkletAnimation-timing-model.svg new file mode 100644 index 00000000..c29d115e --- /dev/null +++ b/css-animation-worklet-1/img/WorkletAnimation-timing-model.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css-animation-worklet-1/img/swipe-to-dismiss-state.png b/css-animation-worklet-1/img/swipe-to-dismiss-state.png new file mode 100644 index 00000000..1f01b8e7 Binary files /dev/null and b/css-animation-worklet-1/img/swipe-to-dismiss-state.png differ diff --git a/css-animation-worklet-1/principles.md b/css-animation-worklet-1/principles.md new file mode 100644 index 00000000..4278df31 --- /dev/null +++ b/css-animation-worklet-1/principles.md @@ -0,0 +1,136 @@ +# Animation Worklet Design Principles and Goals +***for rich interactive effects on the Web Platform*** + + +## Problem and Motivation + +**Fact**: Fact: It is difficult to build smooth rich interactive effects on the web. + +**Why do we care?** +Silky smooth rich interactive user interfaces are now a [basic user expectation][performance] on +modern computing platforms. For web platform to remain competitive it should be capable of high +fidelity, smooth, responsive UI. + + +**Where is the difficulty?** +Three key aspects of rich interactive effects are: smoothness, responsiveness to input (a.k.a. R & A +of [RAILS model][rails]) and their rich interaction model. + +The two main methods for creating animations on the web fall short in at least one of these aspects: + +- CSS (Web) Animations: Aimed at supporting stateless declarative time-driven effects. The + expressiveness is sufficient for common time-based effects. The resulting animation can be smooth + [1](#footnote1). However it's unclear how this model can be scaled to handle the + multi-dimensional inputs, conditional values, and statefulness required by the use cases below. +- requestAnimationFrame: Aimed at creating scripted animation effects. It can support rich + interaction models but it is difficult to make smooth or responsive. This expressiveness of + Javascript coupled with access to all input methods, application state and dom makes this API + capable of building rich interactive effects. However these can only run on main thread alongside + all other scripts[2](#footnote2) which severely hampers their responsiveness and + smoothness. Chrome [studies](https://tdresser.github.io/input-latency-deep-reports/) have shown that script is the main culprit to user responsiveness issues. + +Animation Worklet aims to help bridge the gap between these two. + +## Animation Worklet Vision + +[Animation Worklet][specification] aims to rectify this shortcomings by enabling animations that can +be: + +* rich (imperative, stateful) +* fast-by-default (isolated from main thread) +* respond to rich input e.g., touch, gesture, scroll. + +Animation Worklet is a primitive in the [extensible web](https://extensiblewebmanifesto.org/) spirit. +It exposes browser's fast path to applications in a way that it was never before and reduces browser +magic. + +Examples of rich interactive effects that are (or will be made) possible with Animation Worklet: + + +* Scroll driven effects: + * [Hidey-bar](https://googlechromelabs.github.io/houdini-samples/animation-worklet/twitter-header/): animation depends on both time and scroll input. + * [Parallax](https://googlechromelabs.github.io/houdini-samples/animation-worklet/parallax-scrolling/): Simplest scroll-drive effect. + * [Custom paginated slider](http://aw-playground.glitch.me/amp-scroller.html). + * Pull-to-refresh: animation depends on both touch and time inputs. + * Custom scrollbars. + * [More examples](https://github.com/w3c/css-houdini-drafts/blob/master/scroll-customization-api/UseCases.md) of scroll-driven effects. +* Gesture driven effects: + * [Image manipulator](https://github.com/w3c/csswg-drafts/issues/2493#issuecomment-422153926) that scales, rotates etc. + * Swipe to dismiss. + * Drag-N-Drop. + * Tiled panning e.g., Google maps. +* Stateful script driven effects: + * [Spring-based emulations](https://googlechromelabs.github.io/houdini-samples/animation-worklet/spring-timing/). + * [Spring-Sticky effect](http://googlechromelabs.github.io/houdini-samples/animation-worklet/spring-sticky/). + * Touch-driven physical environments. + * [Expando](http://googlechromelabs.github.io/houdini-samples/animation-worklet/expando/): Procedural animations with multiple elements. +* Animated scroll offsets: + * Having multiple scrollers scroll in sync e.g. diff viewer keeping old/new in sync when you + scroll either ([demo](https://googlechromelabs.github.io/houdini-samples/animation-worklet/sync-scroller/)) + * Custom smooth scroll animations (e.g., physic based fling curves) + + +## First Principle - Richness + +Animation Worklet enables developers to create custom animations by providing an `animate` function +that runs inside animation worklet global scope. The animation logic can take advantage of the full +expressive power of JavaScript, maintain local state, modify and coordinated across many elements. +This new extension point in browser animation system enables richer effects that go well beyond what +can be achieved today with Web Animations and closer to what is possible with requestAnimationFrame. + +**Further explorations in this direction:** Allow richer access to scrolling machinery (e.g., scroll +customization), custom paint worklets, and outputs beyond existing KeyframeEffect interface. + + +## Second Principle - Performance + +Animation Worklet is designed to be thread agnostic. In particular, it can run off main thread +keeping its performance isolated from main thread (also reducing main thread load). + +Animation Worklet API encourages the developers to isolate their critical UI work inside limited +worklet scope with well-specified input and output. This allows user-agent to make much better +scheduling decision about this work and in particular, be much better at maintaining a strict +frame-budget to successfully run these animations on its fast path. Presently the above performance +guarantees are only accessible to a limited set of declarative time-based effects. + +**Further explorations in this direction**: Introduce more sophisticated per-animation scheduling +where a slow animation may run at slower frame-rate without affecting other well-behaved animations, +experiment with translating animation code to native code or even GL shaders moving the computation +to GPU for even stronger performance guarantees! + + +## Third Principle - Interactivity + +Animation Worklet is designed to enable support for animations whose input goes beyond just time, a +single-dimensional variable. + +Web animation timing model is stateless and driven by a single dimensional variable, time. + +Although this model works well for declarative time-based animation, it falls short when it comes to +interactive input-driven effects that are inherently stateful. While it is possible to map simple +forms of input (e.g., [single dimensional scroll](https://wicg.github.io/scroll-animations/#intro)) +into time, it is much more difficult (almost impossible) to do so for multi-dimensional stateful +input such as multi-touch and gesture input. + +Animation Worklet has the necessary expressive power and richness to easily accommodate the full +richness of multi-dimensional input such as touch, gesture, scroll etc. For example it is trivial to +react to scroll phase change, pointer state change, addition/removal of new pointer or state, +calculate pointer velocity, acceleration and other computed values inside an animation worklet. + +**Further explorations in this direction:** Expose pointer and gesture as input to animation +([current proposal](https://github.com/w3c/csswg-drafts/issues/2493#issuecomment-422109535)) + + +# Appendix + + +## Footnotes + +* 1: If authors limit themselves to cheap-to-update properties. In Chrome + these are composited properties e.g., transform, opacity, filter but other engines may have a + slightly difference subset. + + +[performance]: https://paul.kinlan.me/what-news-readers-want/ +[rails]: https://developers.google.com/web/fundamentals/performance/rail#goals-and-guidelines +[specification]: https://github.com/w3c/css-houdini-drafts/tree/master/css-animationworklet diff --git a/css-animation-worklet-1/status.md b/css-animation-worklet-1/status.md new file mode 100644 index 00000000..bdaa1dd3 --- /dev/null +++ b/css-animation-worklet-1/status.md @@ -0,0 +1,18 @@ +# Implementation Status + +## Chrome + +This is a rough sketch of how Chrome plans to deliver Animation Worklet features: + +1. Animation Worktlet Prototype (done): scripted custom animation, single effect, only fast + properties, off-thread. +2. Animation Worktlet [Origin Trial][ot-blogpost] (in progress, [signup][ot-signup]): good + performance, scroll input (ScrollTimeline), basic web-animation controls (play/cancel). +3. Animation Worktlet MVP (in development): animate all properties (slow path ones running in sync + with main thread), multiple effects (i.e., GroupEffect), full web-animation integration. +4. Animation Worktlet V2 (future): touch/gesture input, multiple inputs in single animation, + sophisticated scheduling, other outputs. + +[ot-blogpost]: https://developers.google.com/web/updates/2018/10/animation-worklet +[ot-signup]:https://docs.google.com/forms/d/e/1FAIpQLSfO0_ptFl8r8G0UFhT0xhV17eabG-erUWBDiKSRDTqEZ_9ULQ/viewform + diff --git a/css-layout-api/EXPLAINER.md b/css-layout-api/EXPLAINER.md index a106c788..6aaf3107 100644 --- a/css-layout-api/EXPLAINER.md +++ b/css-layout-api/EXPLAINER.md @@ -65,65 +65,43 @@ There are a lot of things going on in the following example so we'll step throug below. You should read the code below with its explanatory section. ```js -registerLayout('centering', class extends Layout { - static blockifyChildren = true; +registerLayout('centering', class { + async layout(children, edges, constraints, styleMap) { + // (1) Determine our (inner) available size. + const availableInlineSize = constraints.fixedInlineSize - edges.inline; + const availableBlockSize = constraints.fixedBlockSize ? + constraints.fixedBlockSize - edges.block : + null; - static inputProperties = super.inputProperties; - - *layout(constraintSpace, children, styleMap) { - // (1) Resolve our inline size (typically 'width'). - const inlineSize = resolveInlineSize(constraintSpace, styleMap); - - // (2) Determine our (inner) available size. - const bordersAndPadding = resolveBordersAndPadding(constraintSpace, styleMap); - const scrollbarSize = resolveScrollbarSize(constraintSpace, styleMap); - const availableInlineSize = inlineSize - - bordersAndPadding.inlineStart - - bordersAndPadding.inlineEnd - - scrollbarSize.inline; - - const availableBlockSize = resolveBlockSize(constraintSpace, styleMap) - - bordersAndPadding.blockStart - - bordersAndPadding.blockEnd - - scrollbarSize.block; + let maxChildBlockSize = 0; - // (3) Loop over each child and perform layout. const childFragments = []; - const childConstraintSpace = new ConstraintSpace({ - inlineSize: availableInlineSize, - blockSize: availableBlockSize - }); - let maxChildInlineSize = 0; - let maxChildBlockSize = 0; for (let child of children) { - const childFragment = yield child.layoutNextFragment(childConstraintSpace); + // (2) Perform layout upon the child. + const fragment = await child.layoutNextFragment({ + availableInlineSize, + availableBlockSize, + }); - maxChildInlineSize = Math.max(maxChildInlineSize, childFragment.inlineSize); - maxChildBlockSize = Math.max(maxChildBlockSize, childFragment.blockSize); - childFragments.push(childFragment); - } + // Determine the max fragment size so far. + maxChildBlockSize = Math.max(maxChildBlockSize, fragment.blockSize); - // (4) Determine our block size. - const blockOverflowSize = maxChildBlockSize + - bordersAndPadding.blockStart + - bordersAndPadding.blockEnd; - const blockSize = resolveBlockSize(constraintSpace, styleMap, blockOverflowSize); - - // (5) Loop over each fragment to position in the center. - for (let fragment of childFragments) { - fragment.inlineOffset = bordersAndPadding.inlineStart + - (inlineSize - fragment.inlineSize) / 2; - fragment.blockOffset = bordersAndPadding.blockStart + - Math.max(0, (blockSize - fragment.blockSize) / 2); + // Position our child fragment. + fragment.inlineOffset = edges.inlineStart + + (constraints.fixedInlineSize - fragment.inlineSize) / 2; + fragment.blockOffset = edges.blockStart + + Math.max(0, (constraints.fixedBlockSize - fragment.blockSize) / 2); + + childFragments.push(fragment); } - // (6) Return our fragment. + // (3) Determine our "auto" block size. + const autoBlockSize = maxChildBlockSize + edges.block; + + // (4) Return our fragment. return { - inlineSize: inlineSize, - blockSize: blockSize, - inlineOverflowSize: maxChildInlineSize, - blockOverflowSize: blockOverflowSize, - childFragments: childFragments, + autoBlockSize, + childFragments, } } }); @@ -131,12 +109,13 @@ registerLayout('centering', class extends Layout { The `layout` function is your callback into the browsers layout phase in the rendering engine. You are given: - - `constraintSpace`, the space of constraints which the fragment you produce should meet. - `children`, the list of children boxes you should perform layout upon. - - `styleMap`, the style for the current layout. + - `edges`, the size of *your* borders, scrollbar, and padding in the logical coordinate system. + - `constraints`, the constraints which the fragment you produce should meet. + - `style`, the _readonly_ style for the current layout. -Layout eventually will return a dictionary will what the resulting fragment of -that layout should be. +Layout eventually will return a dictionary will what the resulting fragment of that layout should +be. The above example would be used in CSS by: ```css @@ -145,147 +124,128 @@ The above example would be used in CSS by: } ``` -### Step (1) - Resolving the Inline Size ### - -The first thing that you'll want to do for most layouts is to determine your `inlineSize` (`width` -for left-to-right modes). If you want to use the default algorithm you can simply call: -```js -const inlineSize = resolveInlineSize(constraintSpace, styleMap); -``` +### Step (1) - Determine our (inner) available size ### -This will compute the `inlineSize` using the standard CSS rules. E.g. for: -```css -.parent { - width: 100px; -} +The first thing that you'll probably want to do for most layouts is to determine your "inner" size. -.layout { - display: layout(centering); - writing-mode: horizontal-tb; - width: 80%; -} -``` +The `constraints` object passed into the layout function pre-calculates your inline-size (width), +and potentially your block-size (height) if there is enough information to do so (e.g. the element +has `height: 100px` specified). -`resolveInlineSize` will resolve the `inlineSize` of `.layout` to `80px`. See -[developer.mozilla.org](https://developer.mozilla.org) -for an explanation of what [width](https://developer.mozilla.org/en-US/docs/Web/CSS/width) and +See [developer.mozilla.org](https://developer.mozilla.org) for an explanation of what +[width](https://developer.mozilla.org/en-US/docs/Web/CSS/width) and [height](https://developer.mozilla.org/en-US/docs/Web/CSS/height), etc will resolve to. -### Step (2) - Resolving the "inner" Available Size ### - -CSS typically subtracts the border and padding of the current fragment from the available space -provided to the children fragments. Step (2) does this. - -### Step (3) - Perform Layout on `children` ### - -After we have the our `inlineSize` we can now perform layout on our children. - -The first step is to create a constraint space for our child. E.g. +The `edges` object represents the border, scrollbar, and padding of your element. In order to +determine our "inner" size we subtract the `edges.all` from our calculated sizes. For example: ```js -const childConstraintSpace = new ConstraintSpace({ - inlineSize: availableInlineSize, - blockSize: availableBlockSize -}); +const availableInlineSize = constraints.fixedInlineSize - edges.inline; +const availableBlockSize = constraints.fixedBlockSize ? + constraints.fixedBlockSize - edges.block : + null; ``` -There are more options for the constraint space than used here, but for this simple example the -child constraint space just sets the size available to the children. +We keep `availableBlockSize` null if `constraints.fixedBlockSize` wasn't able to be computed. -We now loop through all of our children and perform layout. This is done by: +### Step (2) - Perform layout upon the child ### + +Performing layout on a child can be done with the `layoutNextFragment` method. E.g. ```js -const childFragment = yield child.layoutNextFragment(childConstraintSpace); +const fragment = await child.layoutNextFragment({ + availableInlineSize, + availableBlockSize, +}); ``` -`child` has a very simple API. You can query the style of a child and perform layout - e.g. +The first argument is the "constraints" which you are giving to the child. They can be: + - `availableInlineSize` & `availableBlockSize` - A child fragment will try and "fit" within this + given space. + - `fixedInlineSize` & `fixedBlockSize` - A child fragment will be "forced" to be this size. + - `percentageInlineSize` & `percentageBlockSize` - Percentages will be resolved against this size. -```js -child instanceof LayoutChild; // true -child.styleMap.get('--a-property'); +As layout may be paused or run on a different thread, the API is asynchronous. -const fragment = yield child.layoutNextFragment(childConstraintSpace); -``` - -The result of performing layout on a child is a `Fragment`. A fragment is read-only except for +The result of performing layout on a child is a `LayoutFragment`. A fragment is read-only except for setting the offset relative to the parent fragment. ```js -fragment instanceof Fragment; // true +fragment instanceof LayoutFragment; // true // The resolved size of the fragment. fragment.inlineSize; fragment.blockSize; -// The alignment baseline of the fragment. -fragment.alignmentBaseline; - // We can set the offset relative to the current layout. -frgment.inlineOffset = 10; -frgment.blockOffset = 20; +fragment.inlineOffset = 10; +fragment.blockOffset = 20; ``` -In step (3) we do some additional book-keeping to keep track of the largest child fragment so far. - -### Step (4) - Resolving the Block Size ### +### Step (3) - Determine our "auto" block size ### -Now that we know how large our biggest child is going to be, we can calculate our actual -`blockSize`. +Now that we know how large our biggest child is going to be, we can calculate our "auto" block size. +This is the size the element will be if there are no other block-size constraints (e.g. `height: +100px`). -We perform a call to `resolveBlockSize` again, except this time we also pass in the size that we -would be if the height is `auto`. - -`resolveBlockSize` will also apply rules like `max-height` etc. upon the result. E.g. for: -```css -.layout { - display: layout(centering); - writing-mode: horizontal-tb; - max-height: 200px; -} -``` - -If we called: - - `resolveBlockSize(constraintSpace, styleMap, 400)` the result would be `200`. - - `resolveBlockSize(constraintSpace, styleMap, 180)` the result would be `180`. - -### Step (5) - Positioning our Children Fragments ### - -In this example layout we are centering all of our children. This step sets the offset of the child -relative to the parent fragment. E.g. +In this layout algorithm, we just add the `edges.block` size to the largest child we found: ```js -fragment.inlineOffset = 20; -fragment.blockOffset = 40; +const autoBlockSize = maxChildBlockSize + edges.block; ``` -The above would place `fragment` at an offset `(20, 40)` relative to its parent. - -### Step (6) - Returning our Fragment ### +### Step (4) - Return our fragment ### -Finally we return a dictionary which represents the `Fragment` we wish the rendering engine to -create for us. E.g. +Finally we return a dictionary which represents the fragment we wish the rendering engine to create +for us. E.g. ```js const result = { - inlineSize: inlineSize, - blockSize: blockSize, - inlineOverflowSize: maxChildInlineSize, - blockOverflowSize: blockOverflowSize, - childFragments: childFragments, + autoBlockSize, + childFragments, }; ``` The important things to note here are that you need to explicitly say which `childFragments` you would like to render. If you give this an empty array you won't render any of your children. -Text Layout ------------ +Querying Style +-------------- + +While not present in the "centering" example, it is possible to query the style of the element you +are performing layout for, and all children. E.g. + +```html + + +
+
+
+``` -We used a little trick in the above example: ```js -static blockifyChildren = true; +registerLayout('style-read', class { + static inputProperties = ['--a-number']; + static childInputProperties = ['--a-string']; + + async layout(children, edges, constraints, styleMap) { + // We can read our own style: + styleMap.get('--a-number').value === 42; + + // And our children: + children[0].styleMap.get('--a-string').toString() === 'hello'; + } +}); ``` -This one line forces all of the children to be blockified in your layout. This means for example if -you have: +You can use this to implement properties which your layout depends on, a similar thing that native +layouts use is `flex-grow` for flexbox, or `grid-template-areas` for grid. + +Text Layout +----------- + +By default layouts force all of their children to be blockified. This means for example if you have: ```html
I am some text @@ -293,7 +253,7 @@ you have:
``` -The engine will force the text `I am some text` to be surrounded by a `
`. E.g. +The engine will conceptually force the text `I am some text` to be surrounded by a `
`. E.g. ```html
I am some text
@@ -306,9 +266,17 @@ few native layouts use this trick to simplify their algorithms, for example grid ### Text Fragmentation ### -In the above `centering` example, we forced each `LayoutChild` to produce exactly one `Fragment`. +In the above `centering` example, we forced each `LayoutChild` to produce exactly one +`LayoutFragment`. -However each `LayoutChild` is able to produce more than one `Fragment`. For example: +We are able to ensure children do not blockify by setting the `childDisplay` to `normal`, e.g. +```js +registerLayout('example', class { + static layoutOptions = {childDisplay: 'normal'}; +}); +``` + +Now a `LayoutChild` which represents some text is able to produce more than one `Fragment`. E.g. ```text |---- Inline Available Size ----| @@ -318,8 +286,8 @@ The quick brown fox jumped over the lazy dog. ```js child instanceof LayoutChild; -const fragment1 = yield child.layoutNextFragment(constraintSpace); -const fragment2 = yield child.layoutNextFragment(constraintSpace, fragment1.breakToken); +const fragment1 = yield child.layoutNextFragment(constraints); +const fragment2 = yield child.layoutNextFragment(constraints, fragment1.breakToken); fragment2.breakToken == null; ``` @@ -336,109 +304,38 @@ We pass the `BreakToken` to add back into the `layout()` call in order to produc ### A Basic Text Layout ### ```js -registerLayout('basic-inline', class extends Layout { - static inputProperties = super.inputProperties; - - *layout(constraintSpace, children, styleMap, breakToken) { - // Resolve our inline size. - const inlineSize = resolveInlineSize(constraintSpace, styleMap); +registerLayout('basic-inline', class { + static layoutOptions = {childDisplay: 'normal'}; + async layout(children, edges, constraints, styleMap) { // Determine our (inner) available size. - const bordersAndPadding = resolveBordersAndPadding(constraintSpace, styleMap); - const scrollbarSize = resolveScrollbarSize(constraintSpace, styleMap); - const availableInlineSize = inlineSize - - bordersAndPadding.inlineStart - - bordersAndPadding.inlineEnd - - scrollbarSize.inline; - - const availableBlockSize = resolveBlockSize(constraintSpace, styleMap) - - bordersAndPadding.blockStart - - bordersAndPadding.blockEnd - - scrollbarSize.block; - - const childFragments = []; - let maxInlineSize = 0; + const availableInlineSize = constraints.fixedInlineSize - edges.inline; + const availableBlockSize = constraints.fixedBlockSize !== null ? + constraints.fixedBlockSize - edges.block : null; - let currentLine = []; - let usedInlineSize = 0; - let maxBaseline = 0; + const constraints = { + availableInlineSize, + availableBlockSize, + }; - let lineOffset = 0; - let maxLineBlockSize = 0; - - // Just a small little function which will update the above variables. - const nextLine = function() { - if (usedInlineSize > maxInlineSize) { - maxInlineSize = usedInlineSize; - } - - currentLine = []; - usedInlineSize = 0; - maxBaseline = 0; - - lineOffset += maxLineBlockSize; - maxLineBlockSize = 0; - } - - let childBreakToken = null; - if (breakToken) { - childBreakToken = breakToken.childBreakTokens[0]; - - // Remove all the children we have already produced fragments for. - children.splice(0, children.indexOf(childBreakToken.child)); - } + const childFragments = []; + let blockOffset = edges.blockStart; let child = children.shift(); + let childBreakToken = null; while (child) { - // Make sure we actually have space on the current line. - if (usedInlineSize > availableInlineSize) { - nextLine(); - } - - // The constraint space here will have the inline size of the remaining - // space on the line. - const remainingInlineSize = availableInlineSize - usedInlineSize; - const constraintSpace = new ConstraintSpace({ - inlineSize: availableInlineSize - usedInlineSize, - blockSize: availableBlockSize, - percentageInlineSize: availableInlineSize, - inlineShrinkToFit: true, - }); - - const fragment = yield child.layoutNextFragment(constraintSpace, childBreakToken); + // Layout the next line, the produced line will try and respect the + // availableInlineSize given, you could use this to achieve a column + // effect or similar. + const fragment = await child.layoutNextFragment(constraints, childBreakToken); childFragments.push(fragment); - // Check if there is still space on the current line. - if (fragment.inlineSize > remainingInlineSize) { - nextLine(); + // Position the fragment, note we coulld do something special here, like + // placing all the lines on a "rythimic grid", or similar. + fragment.inlineOffset = edges.inlineStart; + fragment.blockOffset = blockOffset; - // Check if we have gone over the block fragmentation limit. - if (constraintSpace.blockFragmentationType != 'none' && - lineOffset > constraintSpace.blockSize) { - break; - } - } - - // Insert fragment on the current line. - currentLine.push(fragment); - fragment.inlineOffset = usedInlineSize; - - if (fragment.alignmentBaseline > maxBaseline) { - maxBaseline = fragment.alignmentBaseline; - } - - // Go through each of the fragments on the line and update their block - // offsets. - for (let fragmentOnLine of currentLine) { - fragmentOnLine.blockOffset = - lineOffset + maxBaseline - fragmentOnLine.alignmentBaseline; - - const lineBlockSize = - fragmentOnLine.blockOffset + fragmentOnLine.blockSize; - if (maxLineBlockSize < lineBlockSize) { - maxLineBlockSize = lineBlockSize; - } - } + blockOffset += fragment.blockSize; if (fragment.breakToken) { childBreakToken = fragment.breakToken; @@ -450,83 +347,38 @@ registerLayout('basic-inline', class extends Layout { } } - // Determine our block size. - nextLine(); - const blockOverflowSize = lineOffset + - bordersAndPadding.blockStart + - bordersAndPadding.blockEnd; - const blockSize = resolveBlockSize(constraintSpace, styleMap, blockOverflowSize); + // Determine our "auto" block size. + const autoBlockSize = blockOffset + edges.blockEnd; // Return our fragment. - const result = { - inlineSize: inlineSize, - blockSize: blockSize, - inlineOverflowSize: maxInlineSize, - blockOverflowSize: blockOverflowSize, - childFragments: childFragments, - } - - if (childBreakToken) { - result.breakToken = { - childBreakTokens: [childBreakToken], - }; - } - - return result; + return { + autoBlockSize, + childFragments, + }; } }); ``` -The above example is more complex than the previous centering layout because of the ability for text -children to fragment. This example positions all of its children on a line if there is space, if not -it moves to the next. +The above example is slightly more complex than the previous centering layout because of the ability +for text children to fragment. That said it has all the same steps as before: - 1. Resolving the `inlineSize`. - 2. Resolving the (inner) available size. - 3. Performing layout and positioning children fragments. - 4. Resolving the final block size. - 5. Returning the fragment. + 1. Resolving the (inner) available size. + 2. Performing layout and positioning children fragments. + 3. Resolving the "auto" block size. + 4. Returning the fragment. Scrolling --------- We have been handling scrolling in the above example but we haven't talked about it yet. -```js -const scrollbarSize = resolveScrollbarSize(constraintSpace, styleMap); -scrollbarSize.inline; -scrollbarSize.block; -``` - -The above code snippet queries the size of the scrollbar. `resolveScrollbarSize` handles the CSS -`overflow` property. For example if we are `overflow: hidden`, `resolveScrollbarSize` will -report 0 for both directions. - -The overflow size is reported when returning the new fragment, e.g. -```js -return { - inlineSize: 200, - blockSize: 300, - inlineOverflowSize: 400, - blockOverflowSize: 600 -}; -``` -In the above example the scrollable area is double the actual fragments size. - -### `overflow: auto` Behaviour ### +The `edges` object passed into `layout()` respects the `overflow` property. +For example if we are `overflow: hidden`, `edges` object won't include the scrollbar width. -One of the complexities with scrollable fragments is `auto`. In the CSS Layout -API your layout may get called multiple times. E.g. - - If the layout produced a fragment which caused a parent to overflow. - - If the layout produced a fragment with inlineOverflowSize greater than - inlineSize and has `overflow: auto`. In this particular case - `resolveScrollbarSize` will return a different value as scrollbars are now - present. - - Same as above but for the block direction. - -In a future extension of the CSS Layout API there may be the capability to more -efficiently deal with these situations. +For `overflow: auto` the engine will typically perform a layout without a scrollbar, then if it +detects overflow, with a scrollbar. As long as you respect the layout "edges" your layout algorithm +should work as expected. Block Fragmentation ------------------- @@ -556,19 +408,18 @@ In the above example the `multicol` div may produce three (3) fragments. We can make our children fragment by passing them a constraint space with a fragmentation line. E.g. ```js -registerLayout('multi-col', class extends Layout { - static inputProperties = super.inputProperties; - - *layout(constraintSpace, children, styleMap, breakToken) { +registerLayout('special-multi-col', class { + async layout(children, edges, constraints, styleMap, breakToken) { for (let child of children) { // Create a constraint space with a fragmentation line. - const childConstraintSpace = new ConstraintSpace({ - inlineSize: availableInlineSize, - blockSize: availableBlockSize, + const childConstraints = { + availableInlineSize, + availableBlockSize, + blockFragmentationOffset: availableBlockSize, blockFragmentationType: 'column', }); - const fragment = yield child.layoutNextFragment(childConstraintSpace); + const fragment = await child.layoutNextFragment(childConstraints); } // ... @@ -577,19 +428,17 @@ registerLayout('multi-col', class extends Layout { ``` In the above example each of the children will attempt to fragment in the block direction when they -exceed `availableBlockSize`. The type is a `'column'` which will mean it works in conjunction with -rules like `break-inside: avoid-column`. +exceed `blockFragmentationOffset`. The type is a `'column'` which will mean it works in conjunction +with rules like `break-inside: avoid-column`. We can also allow our own layout to be fragmented by respecting the fragmentation line. E.g. ```js -registerLayout('basic-inline', class extends Layout { - static inputProperties = super.inputProperties; - - *layout(constraintSpace, children, styleMap, breakToken) { +registerLayout('basic-inline', class { + async layout(children, edges, constraints, styleMap, breakToken) { // We can check if we need to fragment in the block direction. - if (constraintSpace.blockFragmentationType != 'none') { + if (constraints.blockFragmentationType != 'none') { // We need to fragment! } @@ -602,14 +451,14 @@ registerLayout('basic-inline', class extends Layout { // we'll just use one. child = childToken.child; } else { - child = children; + child = children[0]; } // SNIP! return { - inlineSize: inlineSize, - blockSize: blockSize, + autoBlockSize, + childFragments, breakToken: { data: /* you can place arbitary data here */, childTokens: [childToken] diff --git a/css-layout-api/Overview.bs b/css-layout-api/Overview.bs index 89711646..6074248e 100644 --- a/css-layout-api/Overview.bs +++ b/css-layout-api/Overview.bs @@ -7,6 +7,8 @@ ED: https://drafts.css-houdini.org/css-layout-api-1/ Shortname: css-layout-api Level: 1 Abstract: + An API for allowing web developers to define their own layout modes with javascript. + See EXPLAINER. Editor: Greg Whitworth, gwhit@microsoft.com, w3cid 69511 Editor: Ian Kilpatrick, ikilpatrick@chromium.org, w3cid 73001 Editor: Tab Atkins-Bittner, Google, http://xanthir.com/contact/, w3cid 42199 @@ -47,41 +49,20 @@ spec:infra; type:dfn; text:list spec:html; type:dfn; for:global object; text:realm spec:css22; type:property; text:max-height - text:max-width text:min-height text:min-width +spec:css2; type:property; text:max-width
-urlPrefix: https://heycam.github.io/webidl/; type: dfn;
-    text: InvalidModificationError
-    urlPrefix: #dfn-;
-        url: throw; text: thrown
-    urlPrefix: #idl-;
-        text: boolean
-        text: DOMException
-    url: es-type-mapping; text: converting
-    url: es-invoking-callback-functions; text: Invoke
 urlPrefix: https://tc39.github.io/ecma262/#sec-; type: dfn;
     text: constructor
     text: Construct
-    text: GetMethod
-    text: IsArray
-    text: IsCallable
-    text: IsConstructor
-    text: HasProperty
     url: ecmascript-data-types-and-values; text: type
     url: get-o-p; text: Get
-    url: generatorfunction; text: generator function
     url: terms-and-definitions-function; text: function
     urlPrefix: native-error-types-used-in-this-standard-
         text: TypeError
-urlPrefix: https://drafts.csswg.org/css-display-3/#; type: dfn
-    text: flow layout
-urlPrefix: https://drafts.csswg.org/css-sizing/#; type: dfn
-    text: intrinsic sizes
-urlPrefix: https://drafts.csswg.org/css-break/#; type: dfn
-    text: fragmentation break
 urlPrefix: https://www.w3.org/TR/CSS21/; type:dfn
     urlPrefix: box.html#;
         url: box-dimensions; text: box model edges
@@ -97,11 +78,14 @@ Introduction {#intro}
 
 This section is not normative.
 
-The layout stage of CSS is responsible for generating and positioning fragments from the
-box tree.
+The layout stage of CSS is responsible for generating and positioning [=fragments=] from the [=box
+tree=].
 
-This specification describes an API which allows developers to layout a box in response to
-computed style and box tree changes.
+This specification describes an API which allows developers to layout a [=box=] in response to
+computed style and [=box tree=] changes.
+
+For a high level overview of this API, see the EXPLAINER.
 
 Layout API Containers {#layout-api-containers}
 ==============================================
@@ -112,38 +96,50 @@ to the <> production: layout(<>).
 
layout()
- This value causes an element to generate a layout API container box. + This value causes an element to generate a [=layout API container=] box.
A layout API container is the box generated by an element with a <> -computed value ''layout()''. +[=computed value=] ''layout()''. -A layout API container establishes a new layout API formatting context for its +A [=layout API container=] establishes a new layout API formatting context for its contents. This is the same as establishing a block formatting context, except that the layout provided by the author is used instead of the block layout. For example, floats do not intrude into the layout API container, and the layout API container's margins do not collapse with the margins of its contents. -All inflow children of a layout API container are called layout API children and -are laid out using the auther defined layout. - -Layout API containers form a containing block for their contents -exactly like block +[=Layout API containers=] form a containing block for their contents exactly like block containers do. [[!CSS21]] Note: In a future level of the specification there may be a way to override the containing block behaviour. -The 'overflow' property applies to layout API containers. This is discussed in +The 'overflow' property applies to [=layout API containers=]. This is discussed in [[#interaction-overflow]]. As the layout is entirely up to the author, properties which are used in other layout modes (e.g. -flex, block) may not apply. For example an author may not repect the 'margin' property on children. +flex, block) may not apply. For example an author may not respect the 'margin' property on children. + +
+The HTML below shows an example of setting the ''display'' to a ''layout()'' function, if the CSS +Layout API is supported. + +
+<!DOCTYPE html>
+<style>
+@supports (display: layout(centering)) {
+  .centering-layout { display: layout(centering); }
+}
+</style>
+<div class="centering-layout"></div>
+
+
Layout API Container Painting {#painting} ----------------------------------------- -Layout API Container children paint exactly the same as inline blocks [[!CSS21]], except that +[=Layout API Container=] children paint exactly the same as inline blocks [[!CSS21]], except that the order in which they are returned from the layout method (via {{FragmentResultOptions/childFragments}}) is used in place of raw document order, and 'z-index' values other than ''z-index/auto'' create a stacking context even if 'position' is ''static''. @@ -151,41 +147,39 @@ values other than ''z-index/auto'' create a stacking context even if 'position' Box Tree Transformations {#layout-api-box-tree} ----------------------------------------------- -The layout API children can act in different ways depending on the value of layout options' {{LayoutOptions/childDisplay}} (set by +The inflow children of a [=layout API container=] can act in different ways depending on the value +of [=document layout definition/layout options'=] {{LayoutOptions/childDisplay}} (set by layoutOptions on the class). -If the value of layout options' -{{LayoutOptions/childDisplay}} is "block" the 'display' value of that child is -blockified. This is similar to children of flex containers or grid containers. -See [[!css3-display]]. +If the value of [=document layout definition/layout options'=] {{LayoutOptions/childDisplay}} is +"block" the 'display' value of that child is [=blockified=]. This is similar to +children of [=flex containers=] or [=grid containers=]. See [[!css3-display]]. -If the value of layout options' -{{LayoutOptions/childDisplay}} is "normal", no blockification occurs. Instead -children with a <> computed value of ''inline'' (a root inline box) -will produce a single {{LayoutFragment}} representing each line when -{{LayoutChild/layoutNextFragment()}} is called. +If the value of [=document layout definition/layout options'=] {{LayoutOptions/childDisplay}} is +"normal", no [=blockification=] occurs. Instead children with a <> +[=computed value=] of ''inline'' (a [=root inline box=]) will produce a single {{LayoutFragment}} +representing each line when {{LayoutChild/layoutNextFragment()}} is called. Note: This allows authors to adjust the available inline size of each line, and position each line separately. -Children of a {{LayoutChild}} which represents root inline box also have some additional +Children of a {{LayoutChild}} which represents [=root inline box=] also have some additional transformations. - - A block-level box inside a inline-level box is inlinified I.e. its + - A [=block-level=] box inside a [=inline-level=] box is [=inlinified=] I.e. its <> is set to ''inline''. - - A float inside a inline-level box is not taken out of flow. Instead it must be - treated as inflow, and be inlinified. + - A [=float=] inside a [=inline-level=] box is not taken out of flow. Instead it must be treated as + inflow, and be [=inlinified=]. -In both of the above cases the children become atomic inlines. +In both of the above cases the children become [=atomic inlines=]. Note: User agents would not perform any "inline splitting" or fragmenting when they encounter a - block-level box. + [=block-level=] box.
Note: In the example below "inline-span" would be represented as a single {{LayoutChild}} with - both "block" and "float" being atomic inlines. + both "block" and "float" being [=atomic inlines=].
         <span id="inline-span">
           Text
@@ -196,65 +190,378 @@ Note: User agents would not perform any "inline splitting" or fragmenting when t
     
-Layout API Model and Terminology {#layout-api-model-and-terminology} -==================================================================== +Layout Worklet {#layout-worklet} +================================ + +The {{layoutWorklet}} attribute allows access to the {{Worklet}} responsible for all the classes +which are related to layout. + +The {{layoutWorklet}}'s [=worklet global scope type=] is {{LayoutWorkletGlobalScope}}. + +
+partial namespace CSS {
+    [SameObject] readonly attribute Worklet layoutWorklet;
+};
+
+ +The {{LayoutWorkletGlobalScope}} is the global execution context of the {{layoutWorklet}}. + +
+[Global=(Worklet,LayoutWorklet),Exposed=LayoutWorklet]
+interface LayoutWorkletGlobalScope : WorkletGlobalScope {
+    undefined registerLayout(DOMString name, VoidFunction layoutCtor);
+};
+
+ +
+ Web developers can feature detect by: +
+    if ('layoutWorklet' in CSS) {
+      console.log('CSS Layout API available!');
+    }
+    
+
+ +Concepts {#concepts} +-------------------- + +This section describes internal data-structures created when {{registerLayout(name, layoutCtor)}} is +called. + +A layout definition is a [=struct=] which describes the information needed by the +{{LayoutWorkletGlobalScope}} about the author defined layout (which can be referenced by the +''layout()'' function). It consists of: + + - class constructor which is the class [=constructor=]. + + - layout function which is the layout [=function=] callback. + + - intrinsic sizes function which is the intrinsic sizes + [=function=] callback. + + - constructor valid flag. + + - input properties which is a [=list=] of + DOMStrings. + + - child input properties which is a [=list=] of + DOMStrings. + + - layout options a {{LayoutOptions}}. + +A document layout definition is a [=struct=] which describes the information needed by +the [=document=] about the author defined layout (which can be referenced by the ''layout()'' +function). It consists of: + + - input properties which is a [=list=] of + DOMStrings + + - child input properties which is a [=list=] of + DOMStrings. + + - layout options a {{LayoutOptions}}. + +Registering A Layout {#registering-layout} +------------------------------------------ + +The section describes how a web developer uses {{registerLayout(name, layoutCtor)}} to register a +layout. + +
+[Exposed=LayoutWorklet]
+dictionary LayoutOptions {
+  ChildDisplayType childDisplay = "block";
+  LayoutSizingMode sizing = "block-like";
+};
+
+[Exposed=LayoutWorklet]
+enum ChildDisplayType {
+    "block", // default - "blockifies" the child boxes.
+    "normal",
+};
+
+[Exposed=LayoutWorklet]
+enum LayoutSizingMode {
+    "block-like", // default - Sizing behaves like block containers.
+    "manual", // Sizing is specified by the web developer.
+};
+
+ +The [=document=] has a [=map=] of document layout definitions. Initially this map is +empty; it is populated when {{registerLayout(name, layoutCtor)}} is called. + +The {{LayoutWorkletGlobalScope}} has a [=map=] of layout definitions. Initially this map +is empty; it is populated when {{registerLayout(name, layoutCtor)}} is called. + +Each [=box=] representing a [=layout API container=] has a [=map=] of layout class +instances. Initially this map is empty; it is populated when the user agent calls either +[=determine the intrinsic sizes=] or [=generate a fragment=] for a [=box=]. + +Each [=box=] representing a [=layout API container=] has a styleMap internal slot. +This is a {{StylePropertyMapReadOnly}} which contains the properties listed in +inputProperties. + +The user agent clear the [=styleMap=] internal slot for a [=box=] when: + + - The [=computed values=] of [=document layout definition/input properties=] for the [=box=] + changes. + + - When the [=box=] is removed from the [=box tree=]. + + - Every 1000 layout passes. + + Note: The above rule exists to ensure that web developers do not rely on being able to store + non-regeneratable state on the {{StylePropertyMapReadOnly}} object. + The 1000 limit was picked as a high upper bound, this limit may improve (downwards) over + time. + +
+ Note: The shape of the class should be: +
+        registerLayout('example', class {
+            static inputProperties = ['--foo'];
+            static childrenInputProperties = ['--bar'];
+            static layoutOptions = {
+              childDisplay: 'normal',
+              sizing: 'block-like'
+            };
+
+            async intrinsicSizes(children, edges, styleMap) {
+                // Intrinsic sizes code goes here.
+            }
+
+            async layout(children, edges, constraints, styleMap, breakToken) {
+                // Layout code goes here.
+            }
+        });
+    
+
+ + +The algorithm below is run when the {{registerLayout(name, layoutCtor)}} is called. It notifies the +user agent layout engine about the new user defined layout. + +
+When the registerLayout(|name|, |layoutCtor|) method +is called, the user agent must run the following steps: + 1. If the |name| is an empty string, [=throw=] a [=TypeError=] and abort all these steps. + + 2. Let |layoutDefinitionMap| be {{LayoutWorkletGlobalScope}}'s [=layout definitions=] map. + + 3. If |layoutDefinitionMap|[|name|] [=map/exists=] [=throw=] a "{{InvalidModificationError}}" + {{DOMException}} and abort all these steps. + + 4. Let |inputProperties| be an empty sequence<DOMString>. + + 5. Let |inputPropertiesIterable| be the result of [=Get=](|layoutCtor|, "inputProperties"). + + 6. If |inputPropertiesIterable| is not undefined, then set |inputProperties| to the result of + [=converting=] |inputPropertiesIterable| to a sequence<DOMString>. If an + exception is [=thrown=], rethrow the exception and abort all these steps. + + 7. Filter |inputProperties| so that it only contains [=supported CSS properties=] and [=custom + properties=]. + + Note: The list of CSS properties provided by the input properties getter can either be + custom or native CSS properties. + + Note: The list of CSS properties may contain shorthands. + + Note: In order for a layout class to be forwards compatible, the list of CSS properties can + also contains currently invalid properties for the user agent. For example + margin-bikeshed-property. + + 8. Let |childInputProperties| be an empty sequence<DOMString>. + + 9. Let |childInputPropertiesIterable| be the result of [=Get=](|layoutCtor|, + "childInputProperties"). + + 10. If |childInputPropertiesIterable| is not undefined, then set |childInputProperties| to the + result of [=converting=] |childInputPropertiesIterable| to a + sequence<DOMString>. If an exception is [=thrown=], rethrow the exception + and abort all these steps. + + 11. Filter |childInputProperties| so that it only contains [=supported CSS properties=] and [=custom + properties=]. + + 12. Let |layoutOptionsValue| be the result of [=Get=](|layoutCtor|, "layoutOptions"). + + 13. Let |layoutOptions| be the result of [=converting=] |layoutOptionsValue| to a + {{LayoutOptions}}. If an exception is [=thrown=], rethrow the exception and abort all these + steps. + + 14. Let |prototype| be the result of [=Get=](|layoutCtor|, "prototype"). + + 15. If the result of [=Type=](|prototype|) is not Object, [=throw=] a [=TypeError=] and abort + all these steps. + + 16. Let |intrinsicSizesValue| be the result of [=Get=](|prototype|, "intrinsicSizes"). + + 17. Let |intrinsicSizes| be the result of [=converting=] |intrinsicSizesValue| to the + [=Function=] [=callback function=] type. Rethrow any exceptions from the conversion. + + 18. Let |layoutValue| be the result of [=Get=](|prototype|, "layout"). + + 19. Let |layout| be the result of [=converting=] |layoutValue| to the [=Function=] [=callback + function=] type. Rethrow any exceptions from the conversion. + + 20. Let |definition| be a new [=layout definition=] with: + + - [=class constructor=] being |layoutCtor|. + + - [=layout function=] being |layout|. + + - [=intrinsic sizes function=] being |intrinsicSizes|. + + - [=constructor valid flag=] being true. + + - [=layout definition/child input properties=] being |childInputProperties|. + + - [=layout definition/input properties=] being |inputProperties|. + + - [=layout definition/layout options=] being |layoutOptions|. + + 21. [=map/Set=] |layoutDefinitionMap|[|name|] to |definition|. + + 22. [=Queue a task=] to run the following steps: + + 1. Let |documentLayoutDefinitionMap| be the associated [=document's=] [=document layout + definitions=] [=map=]. + + 2. Let |documentDefinition| be a new [=document layout definition=] with: + + - [=document layout definition/child input properties=] being |childInputProperties|. + + - [=document layout definition/input properties=] being |inputProperties|. + + - [=document layout definition/layout options=] being |layoutOptions|. + + 3. If |documentLayoutDefinitionMap|[|name|] [=map/exists=], run the following steps: + + 1. Let |existingDocumentDefinition| be the result of [=map/get=] + |documentLayoutDefinitionMap|[|name|]. + + 2. If |existingDocumentDefinition| is "invalid", abort all these steps. + + 3. If |existingDocumentDefinition| and |documentDefinition| are not equivalent, (that is + [=document layout definition/input properties=], [=document layout definition/child + input properties=], and [=document layout definition/layout options=] are + different), then: + + [=map/Set=] |documentLayoutDefinitionMap|[|name|] to "invalid". + + Log an error to the debugging console stating that the same class was registered + with different inputProperties, childInputProperties, or + layoutOptions. + + 4. Otherwise, [=map/set=] |documentLayoutDefinitionMap|[|name|] to |documentDefinition|. +
+ +Terminology {#terminology} +-------------------------- -This section gives an overview of the Layout API given to authors. +We define the following terms to be clear about which layout algorithm (formatting context) we are +talking about. -The current layout is the layout algorithm for the box we are currently performing +The current layout is the layout algorithm for the [=box=] we are currently performing layout for. -The parent layout is the layout algorithm for the box's direct parent, (the layout -algorithm which is requesting the current layout to be performed). +The parent layout is the layout algorithm for the [=box's=] direct parent, (the layout +algorithm which is requesting the [=current layout=] to be performed). + +A child layout is the layout algorithm for a {{LayoutChild}} of the [=current layout=]. -A child layout is the layout algorithm for a {{LayoutChild}} of the current layout. +Layout API {#layout-api} +======================== + +This section describes the objects of the Layout API provided to web developers. Layout Children {#layout-children} ---------------------------------- +A {{LayoutChild}} represents a inflow CSS generated [=box=] before layout has occurred. (The box or +boxes will all have a computed value of 'display' that is not ''none''). + +The {{LayoutChild}} does not contain any layout information itself (like inline or block size) but +can be used to generate {{LayoutFragment}}s which do contain layout information. + +An author cannot construct a {{LayoutChild}} with this API, this happens at a separate stage of the +user agent rendering engine (post style resolution). + +An array of {{LayoutChild}}ren is passed into the layout/intrinsicSizes methods which represents the +children of the current box which is being laid out. +
 [Exposed=LayoutWorklet]
 interface LayoutChild {
     readonly attribute StylePropertyMapReadOnly styleMap;
 
-    IntrinsicSizesRequest intrinsicSizes();
-    LayoutFragmentRequest layoutNextFragment(LayoutConstraints constraints, ChildBreakToken breakToken);
+    Promise<IntrinsicSizes> intrinsicSizes();
+    Promise<LayoutFragment> layoutNextFragment(LayoutConstraintsOptions constraints, ChildBreakToken breakToken);
 };
 
The {{LayoutChild}} has internal slot(s): - - \[[box]] a CSS box. + + - \[[box]] a CSS [=box=]. + - \[[styleMap]] a {{StylePropertyMapReadOnly}}, this is the computed style for the child, it is populated with only the properties listed in childInputProperties. -
+ - [[unique id]] the [=unique id=] of the current [=layout + api context=]. This slot is used so that a {{LayoutChild}} used outside the current layout + pass is invalid. -A {{LayoutChild}} represents either a CSS generated box before layout has occured. (The box -or boxes will all have a computed value of 'display' that is not ''none''). +The {{LayoutChild/[[styleMap]]}} may be pre-populated when the [=computed value=] for properties +listed in the in [=layout definition/child input properties=] for the {{LayoutChild/[[box]]}}. -The {{LayoutChild}} does not contain any layout information itself (like inline or block size) but -can be used to generate {{LayoutFragment}}s which do contain layout information. +
+The example below shows the basic usage of a {{LayoutChild}}. +
+registerLayout('example-layout-child', class {
+  static childInputProperties = ['--foo'];
 
-An author cannot construct a {{LayoutChild}} with this API, this happens at a separate stage of the
-rendering engine (post style resolution).
+  async layout(children, edges, constraints, styleMap) {
+
+    // An array of LayoutChildren is passed into both the layout function,
+    // and intrinsic sizes function below.
+    const child = children[0];
+
+    // You can query the any properties listed in "childInputProperties".
+    const fooValue = child.styleMap.get('--foo');
+
+    // And perform layout!
+    const fragment = await child.layoutNextFragment({});
+
+  }
+
+  async intrinsicSizes(children, edges, styleMap) {
+
+    // Or request the intrinsic size!
+    const childIntrinsicSize = await children[0].intrinsicSizes();
+
+  }
+});
+
+
A {{LayoutChild}} could be generated by: - - An element. + - An [=element=]. - - A root inline box. + - A [=root inline box=]. - A ::before or ::after pseudo-element. - Note: Other pseudo-elements such as ::first-letter or ::first-line do not generate - a {{LayoutChild}} for layout purposes. They are additional styling information for a text - node. + Note: Other pseudo-elements such as ::first-letter or ::first-line do not + generate a {{LayoutChild}} for layout purposes. They are additional + styling information for a text node. - - An anonymous box. For example an anonymous box may be inserted as a result of: + - An [=anonymous box=]. For example an anonymous box may be inserted as a result of: - - A text node which has undergone blockification. (Or more generally a root inline - box which has undergone blockification). + - A text node which has undergone [=blockification=]. (Or more generally a [=root inline box=] + which has undergone [=blockification=]). - An element with ''display: table-cell'' which doesn't have a parent with ''display: table''. @@ -272,129 +579,210 @@ A {{LayoutChild}} could be generated by:
Note: As an example the following would be placed into a single {{LayoutChild}} as they share a - root inline box: + [=root inline box=]:
         This is a next node, <span>with some additional styling,
         that may</span> break over<br>multiple lines.
     
-Multiple non-atomic inlines are placed within the same {{LayoutChild}} to allow rendering +Multiple non-[=atomic inlines=] are placed within the same {{LayoutChild}} to allow rendering engines to perform text shaping across element boundaries.
Note: As an example the following should produce one {{LayoutFragment}} but is from - three non-atomic inlines: + three non-[=atomic inlines=]:
         ع<span style="color: blue">ع</span>ع
     
-An array of {{LayoutChild}}ren is passed into the layout method which represents the children of the -current box which is being laid out. +Note: When accessing the {{LayoutChild/styleMap}} the user agent can create a new + {{StylePropertyMapReadOnly}} if none exists yet.
The styleMap, on getting from a {{LayoutChild}} |this|, the user agent must perform the following steps: - 1. Return |this|' {{StylePropertyMapReadOnly}} contained in the {{[[styleMap]]}} internal slot. -
+ 1. If |this|' {{[[styleMap]]}} is null, then: -
-When the layoutNextFragment(|constraints|, |breakToken|) method is -called on a {{LayoutChild}} |this|, the user agent must perform the following steps: + 1. Let |box| be |this|' {{LayoutChild/[[box]]}}. + + 2. Let |definition| be the result of [=get a layout definition=]. + + 3. Let |childInputProperties| be |definition|'s [=layout definition/child input + properties=]. - 1. Let |request| be a new {{LayoutFragmentRequest}} with internal slot(s): - - {{LayoutFragmentRequest/[[layoutChild]]}} set to |this|. - - {{[[layoutConstraints]]}} set to |constraints|. - - {{[[breakToken]]}} set to |breakToken|. + 4. Let |styleMap| be a new {{StylePropertyMapReadOnly}} populated with only the + [=computed values=] for properties listed in |childInputProperties| for |box|. - 2. Return |request|. + 5. Set |this|' {{LayoutChild/[[styleMap]]}} internal slot to |styleMap|. + + Note: If the user agent always pre-populates {{LayoutChild/[[styleMap]]}} then this branch + of the algorithm won't be reached. + + 2. Return |this|' {{StylePropertyMapReadOnly}} contained in the {{LayoutChild/[[styleMap]]}} + internal slot.
+Note: The {{intrinsicSizes()}} method allows the web developer to query the intrinsic sizes of the + {{LayoutChild}}. +
When the intrinsicSizes() method is called on a {{LayoutChild}} |this|, the user agent must perform the following steps: - 1. Let |request| be a new {{IntrinsicSizesRequest}} with internal slot(s): - - {{IntrinsicSizesRequest/[[layoutChild]]}} set to |this|. + 1. Let |p| be a new promise. + + 2. Let |context| be the [=current layout's=] [=layout API context=]. + + 3. If |this|' {{LayoutChild/[[unique id]]}} is not equal to |context|'s [=unique id=], reject + |p| with a "{{InvalidStateError}}" {{DOMException}}, and abort all these steps. + + Note: This is to ensure that only {{LayoutChild}}ren passed in as arguments to either the + layout or intrinsicSizes method are used. + + 4. Let |task| be a new [=layout API work task=] with: + + - [=layout api work task/layout child=] being |this|. - 2. Return |request|. + - [=layout api work task/task type=] being "intrinsic-sizes". + + - [=layout api work task/promise=] being |p|. + + 5. [=list/Append=] |task| to |context|'s [=work queue=]. + + 6. Return |p|.
-Note: Both {{LayoutChild/layoutNextFragment()}} and {{LayoutChild/intrinsicSizes()}} don't - synchronously run. See [[#request-objects]] for a full description. +Note: The {{layoutNextFragment()}} method allows the web developer to produce a {{LayoutFragment}} + for a given {{LayoutChild}} (the result of performing layout). + +
+When the layoutNextFragment(|constraints|, |breakToken|) method is +called on a {{LayoutChild}} |this|, the user agent must perform the following steps: + + 1. Let |p| be a new promise. + + 2. Let |context| be the [=current layout's=] [=layout API context=]. + + 3. If |this|' {{LayoutChild/[[unique id]]}} is not equal to |context|'s [=unique id=], reject + |p| with a "{{InvalidStateError}}" {{DOMException}}, and abort all these steps. + + Note: This is to ensure that only {{LayoutChild}}ren passed in as arguments to either the + layout or intrinsicSizes method are used. + + 4. If |breakToken|'s {{ChildBreakToken/[[unique id]]} is not equal to |context|'s [=unique id=], + reject |p| with a "{{InvalidStateError}}" {{DOMException}}, and abort all these steps. + + 5. If |context|'s [=layout API context/mode=] is "intrinsic-sizes", reject |p| with + a "{{NotSupportedError}}" {{DOMException}}. + + Note: This is to ensure that inside a intrinsicSizes callback, + {{LayoutChild/layoutNextFragment()}} cannot be called. + + 6. Let |task| be a new [=layout API work task=] with: + + - [=layout api work task/layout constraints=] being |constraints|. + + - [=layout api work task/layout child=] being |this|. + + - [=layout api work task/child break token=] being |breakToken|. + + - [=layout api work task/task type=] being "layout". + + - [=layout api work task/promise=] being |p|. + + 7. [=list/Append=] |task| to |context|'s [=work queue=]. + + 8. Return |p|. +
### LayoutChildren and the Box Tree ### {#layout-child-box-tree} -Each box has a \[[layoutChildMap]] internal slot, which is a -map of {{LayoutWorkletGlobalScope}}s to {{LayoutChild}}ren. +Each [=box=] has a \[[layoutChildMap]] internal slot, which is a +[=map=] of {{LayoutWorkletGlobalScope}}s to {{LayoutChild}}ren. + +Note: [=Get a layout child=] returns a {{LayoutChild}} object for the correct + {{LayoutWorkletGlobalScope}} and creates one if it doesn't exist yet.
-When the user agent wants to get a layout child given |workletGlobalScope|, |name|, and -|box|, it must run the following steps: +When the user agent wants to get a layout child given |workletGlobalScope|, |name|, + |box|, and |uniqueId|, it must run the following steps: 1. Assert that: - - |box| is currently attached to the box tree. - - |box|'s containing block is a layout API container. - - The containing block's ''layout()'' function's first argument is |name|. + - |box| is currently attached to the [=box tree=]. + - |box|'s [=containing block=] is a [=layout API container=]. + - The [=containing block's=] ''layout()'' function's first argument is |name|. 2. Let |layoutChildMap| be |box|'s {{[[layoutChildMap]]}}. 3. If |layoutChildMap|[|workletGlobalScope|] does not exist, run the following steps: - 1. Let |definition| be the result of get a layout definition given |name|, and + 1. Let |definition| be the result of [=get a layout definition=] given |name|, and |workletGlobalScope|. - Assert that get a layout definition succeeded, and |definition| is not + Assert that [=get a layout definition=] succeeded, and |definition| is not "invalid". 2. Let |childInputProperties| be |definition|'s child input properties. 3. Let |layoutChild| be a new {{LayoutChild}} with internal slot(s): - - {{[[box]]}} set to |box|. + - {{LayoutChild/[[box]]}} set to |box|. - {{[[styleMap]]}} set to a new {{StylePropertyMapReadOnly}} populated with - only the computed values for properties listed in + only the [=computed values=] for properties listed in |childInputProperties|. 4. Set |layoutChildMap|[|workletGlobalScope|] to |layoutChild|. - 4. Return the result of get |layoutChildMap|[|workletGlobalScope|] + 4. Let |layoutChild| be the result of get |layoutChildMap|[|workletGlobalScope|]. + + 5. Set |layoutChild|'s {{LayoutChild/[[unique id]]}} internal slot to |uniqueId|. + + 6. Return |layoutChild|.
-When a box is inserted into the box tree the user agent may pre-populate the +When a [=box=] is inserted into the [=box tree=] the user agent may pre-populate the {{[[layoutChildMap]]}} for all {{LayoutWorkletGlobalScope}}s. -When a box is removed from the box tree the user agent must clear the +When a [=box=] is removed from the [=box tree=] the user agent must clear the {{[[layoutChildMap]]}}. +The user agent must clear the {{[[layoutChildMap]]}} internal slot every 1000 layout +passes. + +Note: The above rule exists to ensure that web developers do not rely on being able to store + non-regeneratable state on the {{LayoutChild}} object. + The 1000 limit was picked as a high upper bound, this limit may improve (downwards) over time. +
When the user agent wants to update a layout child style given |box|, it must run the following steps: 1. Assert that: - - |box| is currently attached to the box tree. + - |box| is currently attached to the [=box tree=]. - 2. If |box|'s containing block is not a layout API container, abort all these + 2. If |box|'s [=containing block=] is not a [=layout API container=], abort all these steps. 3. Let |layoutChildMap| be |box|'s {{[[layoutChildMap]]}}. 4. For each |layoutChild| in |layoutChildMap|: - 1. Let |styleMap| be |layoutChild|'s {{[[styleMap]]}}. - - 2. Update |styleMap|'s declarations based on the |box|'s new computed style. + 1. |layoutChild|'s {{[[styleMap]]}} to null.
-When the computed style of a box changes the user agent must run the update a layout child -style algorithm. +When the [=computed values=] of [=document layout definition/child input properties=] for a [=box=] +changes the user agent must run the [=update a layout child style=] algorithm. Layout Fragments {#layout-fragments} ------------------------------------ +A {{LayoutFragment}} represents a CSS [=fragment=] of a {{LayoutChild}} after layout has occurred on +that child. This is produced by the {{LayoutChild/layoutNextFragment()}} method. +
 [Exposed=LayoutWorklet]
 interface LayoutFragment {
@@ -411,23 +799,18 @@ interface LayoutFragment {
 
The {{LayoutFragment}} has internal slot(s): - - \[[layoutFragmentRequest]] a - {{LayoutFragmentRequest}}, this is the fragment request which generated this fragment. - - \[[generator]] the generator which produced this - fragment. + - [[unique id]] the [=unique id=] of the [=layout api + context=] which produced this child fragment. This slot is used so that a {{LayoutFragment}} + from a previous layout pass is invalid.
-A {{LayoutFragment}} represents a CSS fragment of a {{LayoutChild}} after layout has occurred -on that child. This is produced by the {{LayoutChild/layoutNextFragment()}} method. - The {{LayoutFragment}} has {{LayoutFragment/inlineSize}} and {{LayoutFragment/blockSize}} attributes, which are set by the respective child's layout algorithm. They represent the border -box size of the CSS fragment, and are relative to the current layout's writing -mode. +box size of the CSS [=fragment=], and are relative to the [=current layout's=] writing mode. The {{LayoutFragment/inlineSize}} and {{LayoutFragment/blockSize}} attributes cannot be changed. If -the current layout requires a different {{LayoutFragment/inlineSize}} or +the [=current layout=] requires a different {{LayoutFragment/inlineSize}} or {{LayoutFragment/blockSize}} the author must perform {{LayoutChild/layoutNextFragment()}} again with different arguments in order to get different results. @@ -435,66 +818,50 @@ The author inside the current layout can position a resulting {{LayoutFragment}} {{LayoutFragment/inlineOffset}} and {{LayoutFragment/blockOffset}} attributes. If not set by the author they default to zero. The {{LayoutFragment/inlineOffset}} and {{LayoutFragment/blockOffset}} attributes represent the position of the {{LayoutFragment}} relative to its parent's border -box, before transform or positioning (e.g. if a fragment is relatively positioned) has +box, before transform or positioning (e.g. if a fragment is [=relatively positioned=]) has been applied. -
-The layout algorithm performs a block-like layout (positioning fragments sequentially in the block -direction), while centering its children in the inline direction. +
+ An example of position a fragment in different writing modes. +
+ A simple visualization showing positioning a {{LayoutFragment}} using + {{LayoutFragment/inlineOffset}} and {{LayoutFragment/blockOffset}} in different writing + modes. +
+
+ + +
+The example below shows the basic usage of a {{LayoutFragment}}. +
+registerLayout('example-layout-fragment', class {
+  async layout(children, edges, constraints, styleMap) {
+
+    // You must perform layout to generate a fragment.
+    const fragment = await child.layoutNextFragment({});
+
+    // You can query the size of the fragment produced:
+    console.log(fragment.inlineSize);
+    console.log(fragment.blockSize);
+
+    // You can set the position of the fragment, e.g. this will set it to the
+    // top-left corner:
+    fragment.inlineOffset = edges.inlineStart;
+    fragment.blockOffset = edges.blockStart;
+
+    // Data may be passed from the child layout:
+    console.log(fragment.data);
+
+    // If the child fragmented, you can use the breakToken to produce the next
+    // fragment in the chain.
+    const nextFragment = await child.layoutNextFragment({}, fragment.breakToken);
+  }
+});
+
+
-
-registerLayout('block-like', class {
-    *intrinsicSizes(children, edges, styleMap) {
-      const childrenSizes = yield children.map((child) => {
-          return child.intrinsicSizes();
-      });
-
-      const maxContentSize = childrenSizes.reduce((max, childSizes) => {
-          return Math.max(max, childSizes.maxContentSize);
-      }, 0) + edges.all.inline;
-
-      const minContentSize = childrenSizes.reduce((max, childSizes) => {
-          return Math.max(max, childSizes.minContentSize);
-      }, 0) + edges.all.inline;
-
-      return {maxContentSize, minContentSize};
-    }
-
-    *layout(children, edges, constraints, styleMap) {
-        const availableInlineSize = constraints.fixedInlineSize - edges.all.inline;
-        const availableBlockSize = (constraints.fixedBlockSize || Infinity) - edges.all.block;
-
-        const childFragments = [];
-        const childConstraints = { availableInlineSize, availableBlockSize };
-
-        const childFragments = yield children.map((child) => {
-            return child.layoutNextFragment(childConstraints);
-        });
-
-        let blockOffset = edges.all.blockStart;
-        for (let fragment of childFragments) {
-            // Position the fragment in a block like manner, centering it in the
-            // inline direction.
-            fragment.blockOffset = blockOffset;
-            fragment.inlineOffset = Math.max(
-                edges.all.inlineStart,
-                (availableInlineSize - fragment.inlineSize) / 2);
-
-            blockOffset += fragment.blockSize;
-        }
-
-        const autoBlockSize = blockOffset + edges.all.blockEnd;
-
-        return {
-            autoBlockSize,
-            childFragments,
-        };
-    }
-});
-
-
- -A layout API container can communicate with other layout API containers by using the +A [=layout API container=] can communicate with other [=layout API containers=] by using the {{LayoutFragment/data}} attribute. This is set by the {{FragmentResultOptions/data}} member in the {{FragmentResultOptions}} dictionary. @@ -503,40 +870,9 @@ fragmented. If the {{LayoutFragment/breakToken}} is null the {{LayoutChild}} won {{LayoutFragment}}s for that token chain. The {{LayoutFragment/breakToken}} can be passed to the {{LayoutChild/layoutNextFragment()}} function to produce the next {{LayoutFragment}} for a particular child. The {{LayoutFragment/breakToken}} cannot be changed. -If the current layout requires a different {{LayoutFragment/breakToken}} the author must perform +If the [=current layout=] requires a different {{LayoutFragment/breakToken}} the author must perform {{LayoutChild/layoutNextFragment()}} again with different arguments. -
-When the user agent wants to create a layout fragment given |generator|, -|layoutFragmentRequest|, and |internalFragment|, it must run the following steps: - 1. Let |targetRealm| be |generator|'s Realm. - - 2. Let |fragment| be a new {{LayoutFragment}} with: - - - The {{[[layoutFragmentRequest]]}} internal slot being |layoutFragmentRequest|. - - - The {{[[generator]]}} internal slot being |generator|. - - - {{LayoutFragment/inlineSize}} being |internalFragment|'s inline size relative to - the current layout's writing mode. - - - {{LayoutFragment/blockSize}} being |internalFragment|'s block size relative to the - current layout's writing mode. - - - {{LayoutFragment/inlineOffset}} initially set to 0. - - - {{LayoutFragment/blockOffset}} initially set to 0. - - - {{LayoutFragment/breakToken}} being a new {{ChildBreakToken}} representing - |internalFragment|'s internal break token, if any. - - - If |internalFragment| has a |clonedData| object stored with it, let - {{LayoutFragment/data}} being the result of - StructuredDeserialize(|clonedData|, |targetRealm|), otherwise null. - - 3. Return |fragment|. -
- Intrinsic Sizes {#intrinsic-sizes} ---------------------------------- @@ -548,18 +884,11 @@ interface IntrinsicSizes { };
-The {{IntrinsicSizes}} object has internal slot(s): - - \[[intrinsicSizesRequest]] a - {{IntrinsicSizesRequest}}, this is the intrinsic sizes request which generated these - intrinsic sizes. - -
- -A {{IntrinsicSizes}} object represents the min-content size and max-content size of a -CSS box. It has {{IntrinsicSizes/minContentSize}} and {{IntrinsicSizes/maxContentSize}} -attributes which represent the border box min/max-content contribution of the {{LayoutChild}} -for the current layout. The attributes are relative to the inline direction of the current -layout's writing mode. +A {{IntrinsicSizes}} object represents the [=min-content size=] and [=max-content size=] of a CSS +[=box=]. It has {{IntrinsicSizes/minContentSize}} and {{IntrinsicSizes/maxContentSize}} attributes +which represent the border box min/max-content contribution of the {{LayoutChild}} for the +[=current layout=]. The attributes are relative to the inline direction of the [=current layout's=] +writing mode. The {{IntrinsicSizes/minContentSize}} and {{IntrinsicSizes/maxContentSize}} cannot be changed. They must not change for a {{LayoutChild}} within the current layout pass. @@ -592,10 +921,10 @@ The example below shows the border-box intrinsic sizes of two children.
 registerLayout('intrinsic-sizes-example', class {
-    *intrinsicSizes(children, edges, styleMap) {
-      const childrenSizes = yield children.map((child) => {
+    async intrinsicSizes(children, edges, styleMap) {
+      const childrenSizes = await Promise.all(children.map((child) => {
           return child.intrinsicSizes();
-      });
+      }));
 
       childrenSizes[0].minContentSize; // 400, (380+10+10) child has a fixed size.
       childrenSizes[0].maxContentSize; // 400, (380+10+10) child has a fixed size.
@@ -604,33 +933,19 @@ registerLayout('intrinsic-sizes-example', class {
       childrenSizes[1].maxContentSize; // 200, size of "XXX XXXX".
     }
 
-    *layout() {}
+    layout() {}
 });
 
-
-When the user agent wants to create an intrinsic sizes object given -|intrinsicSizesRequest|, and |internalIntrinsicSizes|, it must run the following steps: - - 1. Let |intrinsicSizes| be a new {{IntrinsicSizes}} with: - - - The {{[[intrinsicSizesRequest]]}} internal slot being |intrinsicSizesRequest|. - - - {{IntrinsicSizes/minContentSize}} being |internalIntrinsicSizes|' border box - min-content contribution, relative to the current layout's writing mode. - - - {{IntrinsicSizes/maxContentSize}} being |internalIntrinsicSizes|'s border box - max-content contribution, relative to the current layout's writing mode. - - 2. Return |intrinsicSizes|. -
- Layout Constraints {#layout-constraints} ---------------------------------------- +A {{LayoutConstraints}} object is passed into the layout method which represents the all the +constraints for the [=current layout=] to perform layout within. +
-[Constructor(optional LayoutConstraintsOptions options),Exposed=LayoutWorklet]
+[Exposed=LayoutWorklet]
 interface LayoutConstraints {
     readonly attribute double availableInlineSize;
     readonly attribute double availableBlockSize;
@@ -647,138 +962,74 @@ interface LayoutConstraints {
     readonly attribute any data;
 };
 
-dictionary LayoutConstraintsOptions {
-    double availableInlineSize = 0;
-    double availableBlockSize = 0;
-
-    double fixedInlineSize;
-    double fixedBlockSize;
-
-    double percentageInlineSize;
-    double percentageBlockSize;
-
-    double blockFragmentationOffset;
-    BlockFragmentationType blockFragmentationType = "none";
-
-    any data;
-};
-
 enum BlockFragmentationType { "none", "page", "column", "region" };
 
-A {{LayoutConstraints}} object is passed into the layout method which represents the all the -constraints for the current layout to perform layout inside. It is also used to pass -information about the available space into a child layout. - The {{LayoutConstraints}} object has {{LayoutConstraints/availableInlineSize}} and -{{LayoutConstraints/availableBlockSize}} attributes. This represents the available space for -a {{LayoutFragment}} which the layout should respect. +{{LayoutConstraints/availableBlockSize}} attributes. This represents the [=available space=] for the +[=current layout=] to respect. Note: Some layouts may need to produce a {{LayoutFragment}} which exceed this size. For example a - replaced element. The parent layout should expect this to occur and deal with it + [=replaced element=]. The [=parent layout=] should expect this to occur and deal with it appropriately. -A parent layout may require the current layout to be exactly a particular size. If -the {{LayoutConstraints/fixedInlineSize}} or {{LayoutConstraints/fixedBlockSize}} are specified the -current layout should produce a {{LayoutFragment}} with a the specified size in the -appropriate direction. - -
-The layout algorithm performs a flexbox-like distribution of spare space in the inline direction. It -creates child layout constraints which specify that a child should be a fixed inline size. - -
-registerLayout('flex-distribution-like', class {
-    *intrinsicSizes(children, edges, styleMap) {
-      const childrenSizes = yield children.map((child) => {
-          return child.intrinsicSizes();
-      });
-
-      const maxContentSize = childrenSizes.reduce((sum, childSizes) => {
-          return sum + childSizes.maxContentSize;
-      }, 0) + edges.all.inline;
-
-      const minContentSize = childrenSizes.reduce((max, childSizes) => {
-          return sum + childSizes.minContentSize;
-      }, 0) + edges.all.inline;
-
-      return {maxContentSize, minContentSize};
-    }
-
-    *layout(children, edges, constraints, styleMap) {
+A [=parent layout=] may require the [=current layout=] to be exactly a particular size. If the
+{{LayoutConstraints/fixedInlineSize}} or {{LayoutConstraints/fixedBlockSize}} are specified the
+[=current layout=] should produce a {{LayoutFragment}} with a the specified size in the appropriate
+direction.
 
-        const availableInlineSize =
-            constraints.fixedInlineSize - edges.all.inline;
-        const availableBlockSize =
-            (constraints.fixedInlineSize || Infinity) - edges.all.block;
-
-        const childConstraints = { availableInlineSize, availableBlockSize };
+The {{LayoutConstraints}} object has {{LayoutConstraints/percentageInlineSize}} and
+{{LayoutConstraints/percentageBlockSize}} attributes. These represent the size that percentages
+should be resolved against while performing layout.
 
-        const unconstrainedChildFragments = yield children.map((child) => {
-            return child.layoutNextFragment(childConstraints);
-        });
+The {{LayoutConstraints}} has a {{LayoutConstraints/blockFragmentationType}} attribute. The
+[=current layout=] should produce a {{LayoutFragment}} which fragments at the
+{{LayoutConstraints/blockFragmentationOffset}} if possible.
 
-        const unconstrainedSizes = [];
-        const totalSize = unconstrainedChildFragments.reduce((sum, fragment, i) => {
-            unconstrainedSizes[i] = fragment.inlineSize;
-            return sum + fragment.inlineSize;
-        }, 0);
+The [=current layout=] can choose not to fragment a {{LayoutChild}} based on the
+{{LayoutConstraints/blockFragmentationType}}, for example if the child has a property like
+''break-inside: avoid-page;''.
 
-        // Distribute spare space between children.
-        const remainingSpace = Math.max(0, inlineSize - totalSize);
-        const extraSpace = remainingSpace / children.length;
+
+The example below shows the basic usage of the {{LayoutConstraints}} object. - const childFragments = yield children.map((child, i) => { - return child.layoutNextFragment({ - fixedInlineSize: unconstrainedSizes[i] + extraSpace, - availableBlockSize - }); - }); +
+// The class below is registered with a "block-like" sizingMode, and can use the fixedInlineSize,
+// fixedBlockSize attributes.
+registerLayout('layout-constraints-example', class {
+    async layout(children, edges, constraints, styleMap) {
 
-        // Position the fragments.
-        let inlineOffset = 0;
-        let maxChildBlockSize = 0;
-        for (let fragment of childFragments) {
-            fragment.inlineOffset = inlineOffset;
-            fragment.blockOffset = edges.all.blockStart;
+        // Calculate the available size.
+        const availableInlineSize = constraints.fixedInlineSize - edges.inline;
+        const availableBlockSize = constraints.fixedBlockSize ?
+            constraints.fixedBlockSize - edges.inline : null;
 
-            inlineOffset += fragment.inlineSize;
-            maxChildBlockSize = Math.max(maxChildBlockSize, fragment.blockSize);
-        }
+        // Web developers should resolve any percentages against the percentage sizes.
+        const value = constraints.percentageInlineSize * 0.5;
 
-        return {
-            autoBlockSize: maxChildBlockSize + edges.all.block,
-            childFragments,
-        };
     }
 });
 
-The {{LayoutConstraints}} object has {{LayoutConstraints/percentageInlineSize}} and -{{LayoutConstraints/percentageBlockSize}} attributes. These represent the size that a layout -percentages should be resolved against while performing layout. - -The {{LayoutConstraints}} has a {{LayoutConstraints/blockFragmentationType}} attribute. The -current layout should produce a {{LayoutFragment}} which fragments at the -{{LayoutConstraints/blockFragmentationOffset}} if possible. +
-The current layout can choose not to fragment a {{LayoutChild}} based on the -{{LayoutConstraints/blockFragmentationType}}, for example if the child has a property like -''break-inside: avoid-page;''. +The [=create a layout constraints object=] algorithm is used to create the {{LayoutConstraints}} +object. Depending on the {{LayoutOptions/sizing}} it will either pre-calculate the +{{LayoutConstraints/fixedInlineSize}} and {{LayoutConstraints/fixedBlockSize}} upfront.
-When the user agent wants to create a layout constraints object given |sizingMode|, box|, +When the user agent wants to create a layout constraints object given |sizingMode|, |box|, and |internalLayoutConstraints|, it must run the following steps: 1. If |sizingMode| is "block-like" then: 1. Let |fixedInlineSize| be the result of calculating |box|'s border-box - inline size (relative to |box|'s writing mode) exactly like block containers do. + [=inline size=] (relative to |box|'s writing mode) exactly like block containers do. - 2. Let |fixedBlockSize| be null if |box|'s block size is ''height/auto'', otherwise - the result of calculating |box|'s border-box block size exactly like block - containers do. + 2. Let |fixedBlockSize| be null if |box|'s [=block size=] is unable to be calculated at this + stage, (e.g. [=block size=] is ''height/auto''), otherwise the result of calculating + |box|'s border-box [=block size=] exactly like block containers do. 3. Return a new {{LayoutConstraints}} object with: @@ -791,7 +1042,7 @@ and |internalLayoutConstraints|, it must run the following steps: - {{LayoutConstraints/fixedBlockSize}} set to |fixedBlockSize|. - {{LayoutConstraints/availableBlockSize}} set to |fixedBlockSize| if not null, - otherwise |internalLayoutConstraints|' available space in the block axis + otherwise |internalLayoutConstraints|' [=available space=] in the block axis (relative to |box|'s writing mode). - {{LayoutConstraints/percentageBlockSize}} set to |internalLayoutConstraints|' @@ -802,158 +1053,192 @@ and |internalLayoutConstraints|, it must run the following steps: 1. Return a new {{LayoutConstraints}} object with: - {{LayoutConstraints/fixedInlineSize}}/{{LayoutConstraints/fixedBlockSize}} set to |internalLayoutConstraints|' fixed inline/block size (relative to |box|'s writing - mode) imposed by the parent layout. Either may be null. + mode) imposed by the [=parent layout=]. Either may be null. Note: See [[#interaction-sizing]] for different scenarios when this can occur. - {{LayoutConstraints/availableInlineSize}}/{{LayoutConstraints/availableBlockSize}} set - to |internalLayoutConstraints|' available space. + to |internalLayoutConstraints|' [=available space=]. - {{LayoutConstraints/percentageInlineSize}}/{{LayoutConstraints/percentageBlockSize}} set to |internalLayoutConstraints|' percentage resolution size.
-Breaking and Fragmentation {#breaking-and-fragmentation} --------------------------------------------------------- +### Constraints for Layout Children ### {#layout-constraints-children} -
-[Exposed=LayoutWorklet]
-interface ChildBreakToken {
-    readonly attribute BreakType breakType;
-    readonly attribute LayoutChild child;
-};
+The {{LayoutConstraintsOptions}} dictionary represents the set of constraints which can be passed to
+a {{LayoutChild}} to produce a {{LayoutFragment}}.
 
-[Exposed=LayoutWorklet]
-interface BreakToken {
-    readonly attribute sequence<ChildBreakToken> childBreakTokens;
-    readonly attribute any data;
-};
+
+dictionary LayoutConstraintsOptions {
+    double availableInlineSize;
+    double availableBlockSize;
 
-dictionary BreakTokenOptions {
-    sequence<ChildBreakToken> childBreakTokens;
-    any data = null;
-};
+    double fixedInlineSize;
+    double fixedBlockSize;
 
-enum BreakType { "none", "line", "column", "page", "region" };
+    double percentageInlineSize;
+    double percentageBlockSize;
+
+    double blockFragmentationOffset;
+    BlockFragmentationType blockFragmentationType = "none";
+
+    any data;
+};
 
-A {{LayoutChild}} can produce multiple {{LayoutFragment}}s. A {{LayoutChild}} may fragment in the -block direction if a {{LayoutConstraints/blockFragmentationType}} is not none. Additionally -{{LayoutChild}} which represents inline-level content, may fragment line by line if the -layout options' {{LayoutOptions/childDisplay}} (set by -layoutOptions) is "normal". +Note: The [=translate a LayoutConstraintsOptions to internal constraints=] describes how to convert + a {{LayoutConstraintsOptions}} object into a user agents internal representation. -A subsequent {{LayoutFragment}} is produced by using the previous {{LayoutFragment}}'s -{{LayoutFragment/breakToken}}. This tells the child layout to produce a {{LayoutFragment}} -starting at the point encoded in the {{ChildBreakToken}}. +
+When the user agent wants to translate a LayoutConstraintsOptions to internal constraints +given |options|, it must run the following steps: -
-This example shows a simple layout which indents child fragments for a certain number of -lines. + 1. Let the [=available space=] in the inline direction (with respect to the [=current layout=], + be the result of: -This example also demonstrates using the previous {{LayoutFragment/breakToken}} of a -{{LayoutFragment}} to produce the next fragment for the {{LayoutChild}}. + - If |options|' {{LayoutConstraintsOptions/availableInlineSize}} is not null, and + {{LayoutConstraintsOptions/availableInlineSize}} is greater than zero, let the result be + {{LayoutConstraintsOptions/availableInlineSize}}. -It also demonstrates using the {{BreakToken}} to respect the {{LayoutConstraints}}' -{{LayoutConstraints/blockFragmentationType}}, it resumes it layout from the previous {{BreakToken}}. -It returns a {{FragmentResultOptions}} with a {{FragmentResultOptions/breakToken}} which is used to -resume the layout. + - Otherwhise, let the result be zero. -
-registerLayout('indent-lines', class {
-    static layoutOptions = {childDisplay: 'normal'};
-    static inputProperties = ['--indent', '--indent-lines'];
+    2. Let the [=available space=] in the block direction (with respect to the [=current layout=]),
+        be the result of:
 
-    *layout(children, edges, constraints, styleMap, breakToken) {
+        - If |options|' {{LayoutConstraintsOptions/availableBlockSize}} is not null, and
+            {{LayoutConstraintsOptions/availableBlockSize}} is greater than zero, let the result be
+            {{LayoutConstraintsOptions/availableBlockSize}}.
 
-        // Determine our (inner) available size.
-        const availableInlineSize =
-            constraints.fixedInlineSize - edges.all.inline;
-        const availableBlockSize =
-            (constraints.fixedBlockSize || Infinity) - edges.all.block;
+        - Otherwhise, let the result be zero.
 
-        // Detrermine the number of lines to indent, and the indent amount.
-        const indent = resolveLength(constraints, styleMap.get('--indent'));
-        let lines = styleMap.get('--indent-lines').value;
+    3. Let the override size in the inline direction (with respect to the [=current layout=], be the
+        result of:
 
-        const childFragments = [];
+        - Let the result be |options|' {{LayoutConstraintsOptions/fixedInlineSize}}.
 
-        let childBreakToken = null;
-        if (breakToken) {
-            childBreakToken = breakToken.childBreakTokens[0];
+        Note: If the {{LayoutConstraintsOptions/fixedInlineSize}} is null, no override size is
+            applied.
 
-            // Remove all the children we have already produced fragments for.
-            children.splice(0, children.indexOf(childBreakToken.child));
-        }
+    4. Let the override size in the block direction (with respect to the [=current layout=], be the
+        result of:
 
-        let blockOffset = edges.all.blockStart;
-        let child = children.shift();
-        while (child) {
-            const shouldIndent = lines-- > 0;
+        - Let the result be |options|' {{LayoutConstraintsOptions/fixedBlockSize}}.
 
-            // Adjust the inline size for the indent.
-            const childAvailableInlineSize = shouldIndent ?
-                availableInlineSize - indent : availableInlineSize;
+        Note: If the {{LayoutConstraintsOptions/fixedBlockSize}} is null, no override size is
+            applied.
 
-            const childConstraints = {
-                availableInlineSize: childAvailableInlineSize,
-                availableBlockSize,
-                percentageInlineSize: availableInlineSize,
-                blockFragmentationType: constraints.blockFragmentationType,
-            };
+    5. Let the percentage resultion size in the inline direction (with respect to the [=current
+        layout=], be the result of:
 
-            const fragment = yield child.layoutNextFragment(childConstraints,
-                                                            childBreakToken);
-            childFragments.push(fragment);
+        - If |options|' {{LayoutConstraintsOptions/percentageInlineSize}} is not null, and
+            {{LayoutConstraintsOptions/percentageInlineSize}} is greater than zero, let the result
+            be {{LayoutConstraintsOptions/percentageInlineSize}}.
 
-            // Position the fragment.
-            fragment.inlineOffset = shouldIndent ?
-                edges.all.inlineStart + indent : edges.all.inlineStart;
-            fragment.blockOffset = blockOffset;
-            blockOffset += fragment.blockSize;
+        - If |options|' {{LayoutConstraintsOptions/availableInlineSize}} is not null, and
+            {{LayoutConstraintsOptions/availableInlineSize}} is greater than zero, let the result be
+            {{LayoutConstraintsOptions/availableInlineSize}}.
 
-            // Check if we have gone over the block fragmentation limit.
-            if (constraints.blockFragmentationType != 'none' &&
-                blockOffset > constraints.blockSize) {
-                break;
-            }
+        - Otherwhise, let the result be zero.
 
-            if (fragment.breakToken) {
-                childBreakToken = fragment.breakToken;
-            } else {
-                // If a fragment doesn't have a break token, we move onto the
-                // next child.
-                child = children.shift();
-                childBreakToken = null;
-            }
-        }
+    6. Let the percentage resultion size in the block direction (with respect to the [=current
+        layout=], be the result of:
 
-        const autoBlockSize = blockOffset + edges.all.blockEnd;
+        - If |options|' {{LayoutConstraintsOptions/percentageBlockSize}} is not null, and
+            {{LayoutConstraintsOptions/percentageBlockSize}} is greater than zero, let the result
+            be {{LayoutConstraintsOptions/percentageBlockSize}}.
 
-        // Return our fragment.
-        const result = {
-            autoBlockSize,
-            childFragments: childFragments,
-        }
+        - If |options|' {{LayoutConstraintsOptions/availableBlockSize}} is not null, and
+            {{LayoutConstraintsOptions/availableBlockSize}} is greater than zero, let the result be
+            {{LayoutConstraintsOptions/availableBlockSize}}.
 
-        if (childBreakToken) {
-            result.breakToken = {
-                childBreakTokens: [childBreakToken],
-            };
-        }
+        - Otherwhise, let the result be zero.
+
+    7. If the [=child layout=] is a [=layout API container=], then let the store the data (passed by
+        {{LayoutConstraints/data}}) be the result of:
+
+        - Invoking [=StructuredSerializeForStorage=] on |options|'
+            {{LayoutConstraintsOptions/data}}.
+
+ +
+The example below shows the basic usage of the {{LayoutConstraintsOptions}} dictionary. + +
+// The class below is registered with a "block-like" sizingMode, and can use the
+// fixedInlineSize, fixedBlockSize attributes.
+registerLayout('child-layout-constraints-example', class {
+    async layout(children, edges, constraints, styleMap) {
+
+        // The call below gives the child an "available" space. It will try and
+        // fit within this.
+        const fragment = children[0].layoutNextFragment({
+            availableInlineSize: 100,
+            availableBlockSize: 200,
+        });
+
+        // The call below gives the child a "fixed" size, it will be forced to
+        // this size ignoring any style set.
+        const fragment = children[0].layoutNextFragment({
+            fixedInlineSize: 20,
+            fixedBlockSize: 30,
+        });
 
-        return result;
     }
 });
 
+Breaking and Fragmentation {#breaking-and-fragmentation} +-------------------------------------------------------- + +A {{LayoutChild}} can produce multiple {{LayoutFragment}}s. A {{LayoutChild}} may fragment in the +block direction if a {{LayoutConstraints/blockFragmentationType}} is not none. Additionally +{{LayoutChild}} which represents [=inline-level=] content, may fragment line by line if the +layout options' {{LayoutOptions/childDisplay}} (set by +layoutOptions) is "normal". + +
+[Exposed=LayoutWorklet]
+interface ChildBreakToken {
+    readonly attribute BreakType breakType;
+    readonly attribute LayoutChild child;
+};
+
+[Exposed=LayoutWorklet]
+interface BreakToken {
+    readonly attribute FrozenArray<ChildBreakToken> childBreakTokens;
+    readonly attribute any data;
+};
+
+dictionary BreakTokenOptions {
+    sequence<ChildBreakToken> childBreakTokens;
+    any data = null;
+};
+
+enum BreakType { "none", "line", "column", "page", "region" };
+
+ +The {{ChildBreakToken}} has internal slot(s): + - [[unique id]] the [=unique id=] of the [=layout api + context=] which produced this child break token. This slot is used so that a + {{ChildBreakToken}} from a previous layout pass is invalid. + +
+ +A subsequent {{LayoutFragment}} is produced by using the previous {{LayoutFragment}}'s +{{LayoutFragment/breakToken}}. This tells the [=child layout=] to produce a {{LayoutFragment}} +starting at the point encoded in the {{ChildBreakToken}}. + Edges {#edges} -------------- +A {{LayoutEdges}} object is passed into the layout method. This represents the sum of all the [=box +model edges=] (border, scrollbar, padding), for the current box which is being laid out. +
 [Exposed=LayoutWorklet]
-interface LayoutEdgeSizes {
+interface LayoutEdges {
   readonly attribute double inlineStart;
   readonly attribute double inlineEnd;
 
@@ -964,33 +1249,23 @@ interface LayoutEdgeSizes {
   readonly attribute double inline;
   readonly attribute double block;
 };
-
-[Exposed=LayoutWorklet]
-interface LayoutEdges {
-  readonly attribute LayoutEdgeSizes border;
-  readonly attribute LayoutEdgeSizes scrollbar;
-  readonly attribute LayoutEdgeSizes padding;
-
-  readonly attribute LayoutEdgeSizes all;
-};
 
-A {{LayoutEdges}} object is passed into the layout method. This represents the size of the box -model edges for the current box which is being laid out. - -The {{LayoutEdges}} has {{LayoutEdges/border}}, {{LayoutEdges/scrollbar}}, and -{{LayoutEdges/padding}} attributes. Each of these represent the width of their respective edge. +Each of the accessors represents the width in CSS pixels of an edge in each of the [=abstract +dimensions=] ({{LayoutEdges/inlineStart}}, {{LayoutEdges/inlineEnd}}, {{LayoutEdges/blockStart}}, +{{LayoutEdges/blockEnd}}). -The {{LayoutEdges}} has the {{LayoutEdges/all}} attribute. This is a convenience attribute which -represents the sum of the {{LayoutEdges/border}}, {{LayoutEdges/scrollbar}}, {{LayoutEdges/padding}} -edges. +The {{LayoutEdges/inline}}, and {{LayoutEdges/block}} on the {{LayoutEdges}} object are convenience +attributes which represent the sum in that direction. -The {{LayoutEdgeSizes}} object represents the width in CSS pixels of an edge in each of the -abstract dimensions ({{LayoutEdgeSizes/inlineStart}}, {{LayoutEdgeSizes/inlineEnd}}, -{{LayoutEdgeSizes/blockStart}}, {{LayoutEdgeSizes/blockEnd}}). - -The {{LayoutEdgeSizes/inline}}, and {{LayoutEdgeSizes/block}} on the {{LayoutEdgeSizes}} object are -convenience attributes which represent the sum in that direction. +
+ An example of layout edges. +
+ A visualization showing the {{LayoutEdges}} object for a [=box=] with different sized + scrollbar, border, and padding. +
+
This example shows an node styled by CSS, and what its respective {{LayoutEdges}} could contain. @@ -1018,11 +1293,12 @@ This example shows an node styled by CSS, and what its respective {{LayoutEdges}
 registerLayout('box-edges', class {
-    *layout(children, edges, constraints, styleMap, breakToken) {
-        edges.padding.inlineStart; // 5 (as 10% * 50px = 5px).
-        edges.border.blockEnd; // 2
-        edges.scrollbar.inlineEnd; // UA-dependent, may be 0 or >0 (e.g. 16).
-        edges.all.block; // 14 (2 + 5 + 5 + 2).
+    async layout(children, edges, constraints, styleMap, breakToken) {
+        edges.inlineStart; // 2 + 5 (as 10% * 50px = 5px).
+        edges.blockEnd; // 7 (2 + 5)
+        edges.inlineEnd; // UA-dependent, due to scrollbar.
+                         //  Could be 2 + 5 + 0 or 2 + 5 + 16 for example.
+        edges.block; // 14 (2 + 5 + 5 + 2).
     }
 }
 
@@ -1036,31 +1312,31 @@ This section describes how other CSS modules interact with the CSS Layout API. Sizing {#interaction-sizing} ---------------------------- -User agents must use the {{LayoutConstraints}} object to communicate to the current layout -the size they would like the fragment to be. +User agents must use the {{LayoutConstraints}} object to communicate to the [=current layout=] the +size they would like the fragment to be. If the user agent wishes to force a size on the box, it can use the {{LayoutConstraints/fixedInlineSize}} and {{LayoutConstraints/fixedBlockSize}} attributes to do so. -The layout API container can be passed size information in different ways depending on the -value of layout options' {{LayoutOptions/sizing}} (set by +The [=layout API container=] can be passed size information in different ways depending on the value +of layout options' {{LayoutOptions/sizing}} (set by layoutOptions on the class). If the value of layout options' {{LayoutOptions/sizing}} is -"block-like", then the {{LayoutConstraints}} passed to the layout API container: +"block-like", then the {{LayoutConstraints}} passed to the [=layout API container=]: - Must calculate and set {{LayoutConstraints/fixedInlineSize}} based off the rules specified in [[!css-sizing-3]] and the formatting context in which it participates, e.g. - - As a block-level box in a block formatting context, it is sized like a - block box that establishes a formatting context, with an ''width/auto'' inline - size calculated as for non-replaced block boxes. + - As a [=block-level=] box in a [=block formatting context=], it is sized like a [=block + box=] that establishes a formatting context, with an ''width/auto'' [=inline size=] + calculated as for non-replaced block boxes. - - As an inline-level box in an inline formatting context, it is sized as an - atomic inline-level box (such as an inline-block). + - As an [=inline-level=] box in an [=inline formatting context=], it is sized as an atomic + inline-level box (such as an inline-block). - Must calculate and set {{LayoutConstraints/fixedBlockSize}} based off the rules specified in [[!css-sizing-3]], and the formatting context in which it participates. If the - layout API container has an ''height/auto'' block size, and cannot be determined + [=layout API container=] has an ''height/auto'' [=block size=], and cannot be determined ahead of time, {{LayoutConstraints/fixedBlockSize}} must be set to null. If the value of layout options' {{LayoutOptions/sizing}} is @@ -1069,12 +1345,12 @@ If the value of layout options' {{Layout except when it is being forced to a particular size by the formatting context in which it participates, for example: - - If the layout API container is within a block formatting context, is inflow, and - has an ''width/auto'' inline size, the user agent must set the - {{LayoutConstraints/fixedInlineSize}} to the stretch-fit inline size. + - If the [=layout API container=] is within a [=block formatting context=], is inflow, and has + an ''width/auto'' inline size, the user agent must set the + {{LayoutConstraints/fixedInlineSize}} to the [=stretch-fit inline size=].
- Note: In the example below the layout API container has its inline size set to 50. + Note: In the example below the [=layout API container=] has its inline size set to 50.
         <style>
@@ -1097,14 +1373,14 @@ participates, for example:
 
 ### Positioned layout sizing ### {#interaction-sizing-positiong-layout}
 
-If a layout API container is out-of-flow positioned the user agent must solve the
+If a [=layout API container=] is out-of-flow positioned the user agent must solve the
 positioned size equations ([[css-position-3#abs-non-replaced-width]],
 [[css-position-3#abs-non-replaced-height]]), and set the appropriate
 {{LayoutConstraints/fixedInlineSize}} and {{LayoutConstraints/fixedBlockSize}}.
 
 
- Note: In the example below the layout API container has its inline and block size fixed - to 80. + Note: In the example below the [=layout API container=] has its inline and block size fixed to + 80.
         <style>
@@ -1136,16 +1412,16 @@ All positioning in this level of the specification is handled by the user agent.
 As a result:
   - Out-of-flow children do not appear as {{LayoutChild}}ren.
 
-  - Layout API containers establish containing blocks exactly like block
       containers do. [[!CSS21]]
 
   - The {{LayoutFragment/inlineOffset}} and {{LayoutFragment/blockOffset}} represent the position of
       the fragment before any positioning and transforms have occured.
 
-  - The static position of an absolutely-positioned child of a layout API container is
-      set to the inline-start, block-start padding edge of the layout API
-      container. Auto margins are treated as zero for the child.
+  - The [=static position=] of an absolutely-positioned child of a [=layout API container=] is set
+      to the [=inline-start=], [=block-start=] padding edge of the [=layout API container=]. Auto
+      margins are treated as zero for the child.
 
 
Note: In the example below: @@ -1183,20 +1459,20 @@ As a result: Overflow {#interaction-overflow} -------------------------------- -The scrollable overflow for a layout API container is handled by the user agent in -this level of the specification. +The [=scrollable overflow=] for a [=layout API container=] is handled by the user agent in this +level of the specification. -A layout API container should calculate its scrollable overflow exactly like block -containers do. +A [=layout API container=] should calculate its scrollable overflow exactly like block containers +do. -Even if the author's layout API container positions a fragment into the scrollable -overflow region, relative positioning or transforms may cause the fragment to shift such that -its scrollable overflow region, causing no overflow to occur. +Even if the author's [=layout API container=] positions a fragment into the [=scrollable overflow=] +region, relative positioning or transforms may cause the fragment to shift such that its +[=scrollable overflow=] region, causing no overflow to occur. Fragmentation {#interaction-fragmentation} ------------------------------------------ -A parent layout can ask the current layout to fragment by setting the +A [=parent layout=] can ask the [=current layout=] to [=fragment=] by setting the {{LayoutConstraints/blockFragmentationType}} and {{LayoutConstraints/blockFragmentationOffset}}. E.g. [[css-multicol-1]] layout would set a {{LayoutConstraints/blockFragmentationType}} to @@ -1206,7 +1482,7 @@ child to fragment. Alignment {#interaction-alignment} ---------------------------------- -The first/last baseline sets of a layout API container is generated exactly like block +The first/last baseline sets of a [=layout API container=] is generated exactly like block containers do (see [[css-align-3#baseline-export]]). Except that the order of the in-flow children should be determined by the in which they are returned form the layout method (via {{FragmentResultOptions/childFragments}}) instead of the document order. @@ -1217,17 +1493,17 @@ baselines themselves. This will be of the form: To query baseline information from a {{LayoutChild}}.
-const fragment = yield child.layoutNextFragment({
+const fragment = await child.layoutNextFragment({
   fixedInlineSize: availableInlineSize,
   baselineRequests: ['alphabetic', 'middle'],
 });
 fragment.baselines.get('alphabetic') === 25 /* or something */;
 
-To produce baseline information for a parent layout: +To produce baseline information for a [=parent layout=]:
 registerLayout('baseline-producing', class {
-  *layout(children, edges, constraints, styleMap) {
+  async layout(children, edges, constraints, styleMap) {
     const result = {baselines: {}};
 
     for (let baselineRequest of constraints.baselineRequests) {
@@ -1246,622 +1522,386 @@ Layout {#layout}
 
 This section describes how the CSS Layout API interacts with the user agent's layout engine.
 
-Concepts {#concepts}
---------------------
-
-A layout definition is a struct which describes the information needed by the
-{{LayoutWorkletGlobalScope}} about the author defined layout (which can be referenced by the
-''layout()'' function). It consists of:
-
- - class constructor which is the class constructor.
-
- - layout generator function which is the layout generator
-     function callback.
-
- - intrinsic sizes generator function which is the intrinsic sizes
-     generator function callback.
-
- - constructor valid flag.
-
- - input properties which is a list of
-     DOMStrings
-
- - child input properties which is a list of
-     DOMStrings.
-
- - layout options a {{LayoutOptions}}.
-
-A document layout definition is a struct which describes the information needed by
-the document about the author defined layout (which can be referenced by the ''layout()''
-function). It consists of:
-
- - input properties which is a list of
-     DOMStrings
-
- - child input properties which is a list of
-     DOMStrings.
-
- - layout options a {{LayoutOptions}}.
-
-Layout Invalidation {#layout-invalidation}
-------------------------------------------
+Processing Model {#processing-model}
+------------------------------------
 
-Each box has an associated layout valid flag. It may be either
-layout-valid or layout-invalid. It is initially set to layout-invalid.
+A layout API work task is a [=struct=] which describes the information needed by the user
+agent layout engine to perform layout work. It consists of:
 
-Each box has an associated intrinsic sizes valid flag. If may be either
-intrinsic-sizes-valid or intrinsic-sizes-invalid. It is initially set to
-intrinsic-sizes-invalid.
+     - layout constraints a {{LayoutConstraintsOptions}}.
 
-
-When the user agent wants to invalidate layout functions given |box|, the user agent -must run the following steps: + - layout child a {{LayoutChild}}. - 1. Let |layoutFunction| be the ''layout()'' function of the 'display' property on the computed - style for the |box| if it exists. If it is a different type of value (e.g. ''grid'') then - abort all these steps. - - 2. Let |name| be the first argument of the |layoutFunction|. + - child break token a {{ChildBreakToken}}. - 3. Let |documentDefinition| be the result of get a document layout definition given - |name|. + - task type which is either "layout", or + "intrinsic-sizes" - If get a document layout definition returned failure, or if |documentDefinition| is - "invalid", then abort all these steps. + - promise a promise object. - 4. Let |inputProperties| be |documentDefinition|'s input - properties. +A layout API context is a [=struct=] which describes the information needed by the +[=current layout=] to produce either a fragment or determine the intrinsic-sizes for a [=box=]. It +consits of: - 5. Let |childInputProperties| be |documentDefinition|'s child input properties. + - work queue which is a [=list=] of [=layout API work + tasks=]. The user agent will alternate between processing these tasks, and running the + microtask queue. - 6. For each |property| in |inputProperties|, if the |property|'s computed value has - changed, set the layout valid flag on the box to layout-invalid, and - set the intrinsic sizes valid flag to intrinsic-sizes-invalid. + - unique id a internal unique identifier. This is used for + determining that objects exposed to the web developer are only used within the correct + layout pass. E.g. {{LayoutFragment}}s returned in the {{FragmentResultOptions}} dictionary + belong to the current layout pass. - 7. For each |property| in |childInputProperties|, if the |property|'s computed value has - changed, set the layout valid flag on the box to layout-invalid, and - set the intrinsic sizes valid flag to intrinsic-sizes-invalid. -
+ - mode which is either "layout", or + "intrinsic-sizes". This is used for determining what the user agent layout + engine is producing, and if a call to {{LayoutChild/layoutNextFragment()}} is valid. -Invalidate layout functions must be run when the user agent recalculates the computed -style for a box, or when the children's computed style of that box is recalculated. +
+When the user agent wants to create a layout API context given |mode|, it must +run the following steps: -When a child box represented by a {{LayoutChild}} is added or removed from the box -tree or has its layout invalidated (from a computed style change, or a descendant change), -and this invalidation is to be propagated up the box tree, set the layout valid flag -on the current box to layout-invalid and set the intrinsic sizes valid flag on -the current box to intrinsic-sizes-invalid. + 1. Return a new [=layout API context=] with: -When the computed style of a layout API container changes, and this change effects -the values inside the {{LayoutEdges}} object, set the layout valid flag of the box to -layout-invalid, and set the intrinsic sizes valid flag of the box to -intrinsic-sizes-invalid. + - [=work queue=] being a new [=list/empty=] [=list=]. -If the computed style changes effects the values inside the {{LayoutConstraints}} object, just set -the intrinsic sizes valid flag of the box to intrinsic-sizes-invalid. + - [=unique id=] being a unique id. -Note: As the {{LayoutConstraints}} object is only passed into the layout function there is no need - to invalidate the intrinsic sizes. + - [=mode=] being |mode|. -
- Note: As an example the following properties could change the {{LayoutEdges}} object: - - 'padding-top' - - 'border-left-width' - - 'overflow-y' - - And the following properties could change the {{LayoutConstraints}} object: - - 'width' - - 'max-width' - - 'height'
-Note: This only describes layout invalidation as it relates to the CSS Layout API. All - boxes conceptually have a layout valid flag and these changes are propagated - through the box tree. - -Layout Worklet {#layout-worklet} --------------------------------- - -The {{layoutWorklet}} attribute allows access to the {{Worklet}} responsible for all the classes -which are related to layout. - -The {{layoutWorklet}}'s worklet global scope type is {{LayoutWorkletGlobalScope}}. - -
-partial interface CSS {
-    [SameObject] readonly attribute Worklet layoutWorklet;
-};
-
- -The {{LayoutWorkletGlobalScope}} is the global execution context of the {{layoutWorklet}}. - -
-[Global=(Worklet,LayoutWorklet),Exposed=LayoutWorklet]
-interface LayoutWorkletGlobalScope : WorkletGlobalScope {
-    void registerLayout(DOMString name, VoidFunction layoutCtor);
-};
-
+Performing Layout {#performing-layout} +-------------------------------------- -Registering A Layout {#registering-layout} ------------------------------------------- +The section describes how a user agent calls the web developer defined layout to produces intrinsic +sizes, and fragments.
-[Exposed=LayoutWorklet]
-dictionary LayoutOptions {
-  ChildDisplayType childDisplay = "block";
-  LayoutSizingMode sizing = "block-like";
+// This is the final return value from the author defined layout() method.
+dictionary FragmentResultOptions {
+    double inlineSize = 0;
+    double blockSize = 0;
+    double autoBlockSize = 0;
+    sequence<LayoutFragment> childFragments = [];
+    any data = null;
+    BreakTokenOptions breakToken = null;
 };
 
 [Exposed=LayoutWorklet]
-enum ChildDisplayType {
-    "block",
-    "normal",
+interface FragmentResult {
+    constructor(optional FragmentResultOptions options = {});
+    readonly attribute double inlineSize;
+    readonly attribute double blockSize;
 };
 
-[Exposed=LayoutWorklet]
-enum LayoutSizingMode {
-    "block-like",
-    "manual",
+dictionary IntrinsicSizesResultOptions {
+    double maxContentSize;
+    double minContentSize;
 };
 
-The document has a map of document layout definitions. Initially this map -is empty; it is populated when {{registerLayout(name, layoutCtor)}} is called. - -The {{LayoutWorkletGlobalScope}} has a map of layout definitions. Initially this -map is empty; it is populated when {{registerLayout(name, layoutCtor)}} is called. - -Each box representing a layout API container has a map of layout class -instances. Initially this map is empty; it is populated when the user agent calls either -determine the intrinsic sizes or generate a fragment for a box. - -
-When the registerLayout(|name|, |layoutCtor|) method -is called, the user agent must run the following steps: - 1. If the |name| is an empty string, throw a TypeError and abort all these steps. - - 2. Let |layoutDefinitionMap| be {{LayoutWorkletGlobalScope}}'s layout definitions map. - - 3. If |layoutDefinitionMap|[|name|] exists throw a - "InvalidModificationError" DOMException and abort all these steps. - - 4. Let |inputProperties| be an empty sequence<DOMString>. - - 5. Let |inputPropertiesIterable| be the result of Get(|layoutCtor|, "inputProperties"). - - 6. If |inputPropertiesIterable| is not undefined, then set |inputProperties| to the result of - converting |inputPropertiesIterable| to a sequence<DOMString>. If an - exception is thrown, rethrow the exception and abort all these steps. - - Note: The list of CSS properties provided by the input properties getter can either be - custom or native CSS properties. - - Note: The list of CSS properties may contain shorthands. - - Note: In order for a layout class to be forwards compatible, the list of CSS properties can - also contains currently invalid properties for the user agent. For example - margin-bikeshed-property. - - 7. Let |childInputProperties| be an empty sequence<DOMString>. - - 8. Let |childInputPropertiesIterable| be the result of Get(|layoutCtor|, - "childInputProperties"). - - 9. If |childInputPropertiesIterable| is not undefined, then set |childInputProperties| to the - result of converting |childInputPropertiesIterable| to a - sequence<DOMString>. If an exception is thrown, rethrow the exception - and abort all these steps. - - 10. Let |layoutOptionsValue| be the result of Get(|layoutCtor|, "layoutOptions"). - - 11. Let |layoutOptions| be the result of converting |layoutOptionsValue| to a - {{LayoutOptions}}. If an exception is thrown, rethrow the exception and abort all - these steps. - - 12. If the result of IsConstructor(|layoutCtor|) is false, throw a - TypeError and abort all these steps. - - 13. Let |prototype| be the result of Get(|layoutCtor|, "prototype"). - - 14. If the result of Type(|prototype|) is not Object, throw a TypeError and - abort all these steps. - - 15. Let |intrinsicSizes| be the result of Get(|prototype|, - "intrinsicSizes"). - - 16. If the result of IsCallable(|intrinsicSizes|) is false, throw a - TypeError and abort all these steps. - - 17. If |intrinsicSizes|'s \[[FunctionKind]] internal slot is not - "generator", throw a TypeError and abort all these steps. - - 18. Let |layout| be the result of Get(|prototype|, "layout"). - - 19. If the result of IsCallable(|layout|) is false, throw a TypeError and - abort all these steps. +The {{FragmentResult}} has internal slot(s): - 20. If |layout|'s \[[FunctionKind]] internal slot is not "generator", - throw a TypeError and abort all these steps. + - \[[box]] a CSS [=box=]. - 21. Let |definition| be a new layout definition with: + - [[inline size]] the inline size of the resulting + fragment. - - class constructor being |layoutCtor|. + - [[block size]] the block size of the resulting + fragment. - - layout generator function being |layout|. + - [[child fragments]] the list of child fragments. - - intrinsic sizes generator function being |intrinsicSizes|. + - \[[data]] some optional serialized data. - - constructor valid flag being true. + - [[internal break token]] an internal representation of + the break information for this fragment. - - input properties being |inputProperties|. + - [[unique id]] the [=unique id=] of the current + [=layout api context=]. This slot is used so that a {{FragmentResult}} used outside the + current layout pass is invalid. - - child input properties being |childInputProperties|. +
- - layout options being |layoutOptions|. +The web developer defined layout method can return either a {{FragmentResultOptions}} or a +{{FragmentResult}}. The {{FragmentResult}} can be used for determining the final size of the +fragment or detecting if the provided {{FragmentResultOptions}} would result in triggering a +fallback to [=flow layout=]. - 22. Set |layoutDefinitionMap|[|name|] to |definition|. +
+This example show the web developer using the {{FragmentResult}} instead of just returning the +{{FragmentResultOptions}} object. - 23. Queue a task to run the following steps: +
+registerLayout('feature-detection', class {
+    async layout(children, edges, constraints, styleMap, breakToken) {
+
+      let result;
+      try {
+        result = new FragmentResult({
+          childFragments: [],
+          autoBlockSize: 100
+        });
+      } catch (e) {
+        // The above call may throw, if the dictionary was just returned, it
+        //  would fallback to flow layout.
+      }
 
-        1. Let |documentLayoutDefinitionMap| be the associated document's document layout
-            definitions map.
+      // The web developer can test what size the fragment will be.
+      result.blockSize;
 
-        2. Let |documentDefinition| be a new document layout definition with:
+      // Instead of returning the dictionary, we can just return this object.
+      return result;
+    }
+}
+
+
- - input properties being |inputProperties|. +
+The inlineSize, on getting from a {{FragmentResult}} |this|, +the user agent must perform the following steps: - - child input properties being - |childInputProperties|. + 1. Return |this|' {{FragmentResult/[[inline size]]}} internal slot. +
- - layout options being |layoutOptions|. +
+The blockSize, on getting from a {{FragmentResult}} |this|, +the user agent must perform the following steps: - 3. If |documentLayoutDefinitionMap|[|name|] exists, run the following steps: + 1. Return |this|' {{FragmentResult/[[block size]]}} internal slot. +
- 1. Let |existingDocumentDefinition| be the result of get - |documentLayoutDefinitionMap|[|name|]. +
+When the FragmentResult(|options|) constructor is called, +the user agent must perform the following steps: - 2. If |existingDocumentDefinition| is "invalid", abort all these steps. + 1. Let |context| be the [=current layout's=] [=layout API context=]. - 3. If |existingDocumentDefinition| and |documentDefinition| are not equivalent, (that is - input properties, child input properties, and layout options are different), then: + 2. Return the result of [=construct a fragment result=] given |context|, and |options|. - Set |documentLayoutDefinitionMap|[|name|] to "invalid". +
- Log an error to the debugging console stating that the same class was registered - with different inputProperties, childInputProperties, or - layoutOptions. +Note: The [=construct a fragment result=] algorithm performs a series of validation checks (the + web developer isn't using an object from a previous invocation, and determines the final size of + the resulting fragment. - 4. Otherwise, set |documentLayoutDefinitionMap|[|name|] to - |documentDefinition|. +
+When the user agent wants to construct a fragment result given |context|, and |options| +the user agent must perform the following steps: -
- Note: The shape of the class should be: -
-        class MyLayout {
-            static get inputProperties() { return ['--foo']; }
-            static get childrenInputProperties() { return ['--bar']; }
-            static get layoutOptions() {
-              return {childDisplay: 'normal', sizing: 'block-like'}
-            }
+    1. Let |uniqueId| be |context|'s [=unique id=].
 
-            *intrinsicSizes(children, edges, styleMap) {
-                // Intrinsic sizes code goes here.
-            }
+    2. Let |box| be the [=current layout's=] [=box=].
 
-            *layout(children, edges, constraints, styleMap, breakToken) {
-                // Layout code goes here.
-            }
-        }
-    
-
-
- -Layout Engine {#layout-engine} ------------------------------- + 3. Let |breakTokenOptions| be |options|'s {{FragmentResultOptions/breakToken}}. -
- Issue: Currently the API is in an iterable generator form. Based on implementation experience, and - web developer experience, this may change to promise based API instead. There are both pros and - cons to each of these. + 4. [=list/For each=] |childFragment| in |options|'s {{FragmentResultOptions/childFragments}}, + perform the following stubsteps: - Promises - - Better error reporting. - - Potentially better developer ergonomics. + 1. If |childFragment|'s {{LayoutFragment/[[unique id]]}} internal slot is not equal to + |uniqueId|, then [=throw=] a [=TypeError=], and abort all these steps. - Generator - - More "strict" - can only perform layout operations. Don't have to restrict which promise APIs - work each call. - - Potentially better bindings overhead. -
+ 5. [=list/For each=] |childBreakToken| in |breakTokenOptions|'s + {{BreakTokenOptions/childBreakTokens}}, perform the following stubsteps: -### Request Objects ### {#request-objects} + 1. If |childBreakToken|'s {{ChildBreakToken/[[unique id]]}} internal slot is not equal to + |uniqueId|, then [=throw=] a [=TypeError=], and abort all these steps. -
-[Exposed=LayoutWorklet]
-interface IntrinsicSizesRequest {
-};
-
-[Exposed=LayoutWorklet]
-interface LayoutFragmentRequest {
-};
-
-typedef (IntrinsicSizesRequest or LayoutFragmentRequest)
-    LayoutFragmentRequestOrIntrinsicSizesRequest;
-
+ 6. If |sizingMode| is "block-like": -The {{IntrinsicSizesRequest}} has internal slot(s): - - \[[layoutChild]] a {{LayoutChild}}, this is the - child which the intrinsic sizes must be calculated for. + - Then: -The {{LayoutFragmentRequest}} has internal slot(s): - - \[[layoutChild]] a {{LayoutChild}}, this is the - child which the fragment must be generated for. - - \[[layoutConstraints]] a - {{LayoutConstraintsOptions}} dictionary, these are the input constraints to the - {{LayoutChild}}'s layout algorithm. - - \[[breakToken]] a {{ChildBreakToken}} object, - which is the break token the layout must be resumed with. + 1. Let |inlineSize| be the result of calculating |box|'s border-box [=inline + size=] (relative to |box|'s writing mode) exactly like block containers do. -
+ 2. Let |blockSize| be the result of calculating |box|'s border-box + [=block size=] (relative to |box|'s writing mode) exactly like block containers do, + given |fragment|'s {{FragmentResultOptions/autoBlockSize}} as the "intrinsic + block size". -The layout method and intrinsic sizes method on the author supplied layout class is a generator -function instead of a regular javascript function. This is for user-agents to be able to support -asynchronous and parallel layout engines. + - Otherwise (|sizingMode| is "manual"): -When an author invokes the {{LayoutChild/layoutNextFragment()}} method on a {{LayoutChild}} the -user-agent doesn't synchronously generate a {{LayoutFragment}} to return to the author's code. -Instead it returns a {{LayoutFragmentRequest}}. This is a completely opaque object to the author but -contains internal slots which encapsulates the {{LayoutChild/layoutNextFragment()}} method call. + 1. Let |inlineSize| be |fragment|'s {{FragmentResultOptions/inlineSize}}. -When a {{LayoutFragmentRequest}}(s) are yielded from a layout generator object the user-agent's -layout engine may run the algorithm asynchronously with other work, and/or on a different thread of -execution. When {{LayoutFragment}}(s) have been produced by the engine, the user-agent will "tick" -the generator object with the resulting {{LayoutFragment}}(s). + 2. Let |blockSize| be |fragment|'s {{FragmentResultOptions/blockSize}}. -The same applies for the {{LayoutChild/intrinsicSizes()}} method. + 7. Let |clonedData| be the result of invoking [=StructuredSerializeForStorage=] on |options|'s + {{FragmentResultOptions/data}}. -
-An example layout engine written in javascript is shown below. + 8. Let |clonedBreakTokenData| be the result of invoking [=StructuredSerializeForStorage=] on + |breakTokenOptions|'s {{BreakTokenOptions/data}}. -
-class LayoutEngine {
-  // This function takes the root of the box-tree, a LayoutConstraints object, and a
-  // BreakToken to (if paginating for printing for example) and generates a
-  // LayoutFragment.
-  layoutEntry(rootBox, rootPageConstraints, breakToken) {
-    return layoutFragment({
-      box: rootBox,
-      layoutConstraints: rootPageConstraints,
-      breakToken: breakToken,
-    });
-  }
+    9. Let |internalBreakToken| be the internal representation of the [=fragmentation break=]
+        containing |clonedBreakTokenData|, and |breakTokenOptions|.
 
-  // This function takes a LayoutFragmentRequest and calls the appropriate
-  // layout algorithm to generate the a LayoutFragment.
-  layoutFragment(fragmentRequest) {
-    const box = fragmentRequest.layoutChild;
-    const algorithm = selectLayoutAlgorithmForBox(box);
-    const fragmentRequestGenerator = algorithm.layout(
-        fragmentRequest.layoutConstraints,
-        box.children,
-        box.styleMap,
-        fragmentRequest.breakToken);
-
-    let nextFragmentRequest = fragmentRequestGenerator.next();
-
-    while (!nextFragmentRequest.done) {
-      // A user-agent may decide to perform layout to generate the fragments in
-      // parallel on separate threads. This example performs them synchronously
-      // in order.
-      let fragments = nextFragmentRequest.value.map(layoutFragment);
-
-      // A user-agent may decide to yield for other work (garbage collection for
-      // example) before resuming this layout work. This example just performs
-      // layout synchronously without any ability to yield.
-      nextFragmentRequest = fragmentRequestGenerator.next(fragments);
-    }
+    10. Return a new {{FragmentResult}} with:
 
-    return nextFragmentRequest.value; // Return the final LayoutFragment.
-  }
-}
-
-
+ - {{FragmentResult/[[box]]}} being |box|. -Performing Layout {#performing-layout} --------------------------------------- + - {{FragmentResult/[[inline size]]}} being |inlineSize|. -
-// This is the final return value from the author defined layout() method.
-dictionary FragmentResultOptions {
-    double inlineSize = 0;
-    double blockSize = 0;
-    double autoBlockSize = 0;
-    sequence<LayoutFragment> childFragments = [];
-    any data = null;
-    BreakTokenOptions breakToken = null;
-};
+        - {{FragmentResult/[[block size]]}} being |blockSize|.
 
-dictionary IntrinsicSizesResultOptions {
-    double maxContentSize;
-    double minContentSize;
-};
-
+ - {{FragmentResult/[[child fragments]]}} being |options|'s + {{FragmentResultOptions/childFragments}}. -### Determining Intrinsic Sizes ### {#determining-intrinsic-sizes} + - {{FragmentResult/[[data]]}} being |clonedData|. -The determine the intrinsic sizes algorithm defines how a user agent is to query the author -defined layout for a box's intrinsic sizes information. + - {{FragmentResult/[[internal break token]]}} being |internalBreakToken|. -Note: The determine the intrinsic sizes algorithm allows for user agents to cache an arbitary - number of previous invocations to reuse. + - {{FragmentResult/[[unique id]]}} being |uniqueId|. -
-When the user agent wants to determine the intrinsic sizes of a layout API formatting -context for a given |box|, |childBoxes| it must run the following steps: +
- 1. Let |layoutFunction| be the ''layout()'' for the computed value of <> - for |box|. +### Determining Intrinsic Sizes ### {#determining-intrinsic-sizes} - 2. If the intrinsic sizes valid flag for the |layoutFunction| is - intrinsic-sizes-valid the user agent may use the intrinsic sizes from the - previous invocation. If so it may abort all these steps and use the previous value - for the intrinsic sizes. +The [=determine the intrinsic sizes=] algorithm defines how a user agent is to query the author +defined layout for a [=box's=] [=intrinsic sizes=] information. - 3. Set the intrinsic sizes valid flag for the |layoutFunction| to - intrinsic-sizes-valid. +Note: The [=determine the intrinsic sizes=] algorithm allows for user agents to cache an arbitrary + number of previous invocations to reuse. - 4. Let |name| be the first argument of the |layoutFunction|. +
+When the user agent wants to determine the intrinsic sizes of a [=layout API formatting +context=] for a given |box|, |childBoxes| it must run the following steps: - 5. Let |documentDefinition| be the result of get a document layout definition given - |name|. + 1. Let |layoutFunction| be the ''layout()'' for the [=computed value=] of <> for + |box|. - If get a document layout definition returned failure, or if |documentDefinition| is - "invalid", then let |box| fallback to the flow layout and abort all - these steps. + 2. Let |name| be the first argument of the |layoutFunction|. - 6. Let |workletGlobalScope| be a {{LayoutWorkletGlobalScope}} from the list of worklet's - WorkletGlobalScopes from the layout {{Worklet}}. + 3. Let |documentDefinition| be the result of [=get a document layout definition=] given |name|. - The user agent must have, and select from at least two - {{LayoutWorkletGlobalScope}}s in the worklet's WorkletGlobalScopes list, - unless the user agent is under memory constraints. + If [=get a document layout definition=] returned failure, or if |documentDefinition| is + "invalid", then let |box| fallback to the [=flow layout=] and abort all these + steps. - Note: This is to ensure that authers do not rely on being able to store state on the global - object or non-regeneratable state on the class. + 4. Let |workletGlobalScope| be a {{LayoutWorkletGlobalScope}} from the list of [=worklet's + WorkletGlobalScopes=] from the layout {{Worklet}}, following the rules defined in + [[#global-scope-selection]]. - The user agent may also create a WorkletGlobalScope at this time, given the + The user agent may also [=create a WorkletGlobalScope=] at this time, given the layout {{Worklet}}. - 7. Run invoke a intrinsic sizes callback given |name|, |box|, |childBoxes|, and - |workletGlobalScope| optionally in parallel. + 5. Run [=invoke an intrinsic sizes callback=] given |name|, |box|, |childBoxes|, and + |workletGlobalScope| optionally [=in parallel=]. - Note: If the user agent runs invoke a intrinsic sizes callback on a thread in - parallel, it should select a layout worklet global scope which can be used on that + Note: If the user agent runs [=invoke an intrinsic sizes callback=] on a thread [=in + parallel=], it should select a layout worklet global scope which can be used on that thread.
-
-When the user agent wants to invoke a intrinsic sizes callback given |name|, |box|, +
+When the user agent wants to invoke an intrinsic sizes callback given |name|, |box|, |childBoxes|, and |workletGlobalScope|, it must run the following steps: - 1. Let |definition| be the result of get a layout definition given |name|, and + 1. Let |definition| be the result of [=get a layout definition=] given |name|, and |workletGlobalScope|. - If get a layout definition returned failure, let the |box| fallback to the flow - layout and abort all these steps. + If [=get a layout definition=] returned failure, let the |box| fallback to the [=flow + layout=] and abort all these steps. - 2. Let |layoutInstance| be the result of get a layout class instance given |box|, + 2. Let |layoutInstance| be the result of [=get a layout class instance=] given |box|, |definition|, |workletGlobalScope|. - If get a layout class instance returned failure, let the |box| fallback to the - flow layout and abort all these steps. + If [=get a layout class instance=] returned failure, let the |box| fallback to the [=flow + layout=] and abort all these steps. - 3. Let |inputProperties| be |definition|'s input properties. + 3. Let |inputProperties| be |definition|'s [=layout definition/input properties=]. - 4. Let |children| be a new list. + 4. Let |children| be a new [=list=]. - 5. For each |childBox| in |childBoxes| perform the following substeps: + 5. [=list/For each=] |childBox| in |childBoxes| perform the following substeps: - 1. Let |layoutChild| be the result of get a layout child given |workletGlobalScope|, - |name|, and |childBox|. + 1. Let |layoutChild| be the result of [=get a layout child=] given |workletGlobalScope|, + |name|, |childBox|, and |context|'s [=unique id=]. - 2. Append |layoutChild| to |children|. + 2. [=list/Append=] |layoutChild| to |children|. - 6. Let |edges| be a new {{LayoutEdgeSizes}} populated with the computed value for all the - box model edges for |box|. + 6. Let |edges| be a new {{LayoutEdges}} populated with the [=computed value=] for all the [=box + model edges=] for |box|. - 7. Let |styleMap| be a new {{StylePropertyMapReadOnly}} populated with only the - computed values for properties listed in |inputProperties| for |box|. + 7. Let |styleMap| be the result of [=get a style map=] given |box|, and |inputProperties|. - Issue: We may want to store |styleMap| on |box| instead, similar to |layoutInstance|. + 8. At this stage the user agent may re-use the [=intrinsic sizes=] from a previous invocation if + |children|, |edges|, and |styleMap| are equivalent to that previous invocation. If so let + the intrinsic sizes the cached intrinsic sizes and abort all these steps. - 8. At this stage the user agent may re-use the intrinsic sizes from a previous invocation - if |children|, |styleMap| are equivalent to that previous invocation. If so let the - intrinsic sizes the cached intrinsic sizes and abort all these steps. + 9. Let |context| be the result of [=create a layout API context=] given + "intrinsic-sizes". - 9. Let |intrinsicSizesGeneratorFunction| be |definition|'s intrinsic sizes generator - function. + 10. Let |intrinsicSizesFunction| be |definition|'s [=intrinsic sizes function=]. - 10. Let |intrinsicSizesGenerator| be the result of - Invoke(|intrinsicSizesGeneratorFunction|, |layoutInstance|, «|children|, |edges|, - |styleMap|»). + 11. Let |value| be the result of [=Invoke=](|intrinsicSizesFunction|, |layoutInstance|, + «|children|, |edges|, |styleMap|»). - If an exception is thrown the let |box| fallback to the flow layout and abort - all these steps. + If an exception is [=thrown=] the let |box| fallback to the [=flow layout=] and abort all + these steps. - 11. Let |intrinsicSizesValue| be the result of run a generator given - |intrinsicSizesGenerator|, and "intrinsic-sizes". + 12. If |value| is a promise: - If run a generator returned failure, then let |box| fallback to the flow - layout and abort all these steps. + - Then: - 12. Let |intrinsicSizes| be the result of converting |intrinsicSizesValue| to a - {{IntrinsicSizesResultOptions}}. If an exception is thrown, let |box| fallback to the - flow layout and abort all these steps. + 1. Let |intrinsicSizesValue| be the result of [=run a work queue=] given |value|, and + |context|'s [=work queue=]. - 13. Set the intrinsic sizes of |box|: + If [=run a work queue=] returns failure, let the |box| fallback to the [=flow + layout=] and abort all these steps. - - Let |intrinsicSizes|'s {{IntrinsicSizesResultOptions/minContentSize}} be the - min-content size of |box|. + - Otherwise: - - Let |intrinsicSizes|'s {{IntrinsicSizesResultOptions/maxContentSize}} be the - max-content size of |box|. -
+ 1. Let |intrinsicSizesValue| be |value|. -### Generating Fragments ### {#generating-fragments} + 13. Let |intrinsicSizes| be the result of [=converting=] |intrinsicSizesValue| to a + {{IntrinsicSizesResultOptions}}. If an exception is [=thrown=], let |box| fallback to the + [=flow layout=] and abort all these steps. -The generate a fragment algorithm defines how a user agent is to generate a box's -fragment for an author defined layout. + 14. Set the [=intrinsic sizes=] of |box|: -Note: The generate a fragment algorithm allows for user agents to cache an arbitary number of - previous invocations to reuse. + - Let |intrinsicSizes|'s {{IntrinsicSizesResultOptions/minContentSize}} be the [=min-content + size=] of |box|. -
-When the user agent wants to generate a fragment of a layout API formatting -context for a given |box|, |childBoxes|, |internalLayoutConstraints|, and an optional -|internalBreakToken| it must run the following steps: + - Let |intrinsicSizes|'s {{IntrinsicSizesResultOptions/maxContentSize}} be the [=max-content + size=] of |box|. +
- 1. Let |layoutFunction| be the ''layout()'' for the computed value of <> - for |box|. +### Generating Fragments ### {#generating-fragments} - 2. If the layout valid flag for the |layoutFunction| is layout-valid the user - agent may use the intrinsic sizes from the previous invocation. If so it - may abort all these steps and use the previous value for the intrinsic sizes. +The [=generate a fragment=] algorithm defines how a user agent is to generate a [=box's=] +[=fragment=] for an author defined layout. - 3. Set the layout valid flag for the |layoutFunction| to layout-valid. +Note: The [=generate a fragment=] algorithm allows for user agents to cache an arbitrary number of + previous invocations to reuse. - 4. Let |name| be the first argument of the |layoutFunction|. +
+When the user agent wants to generate a fragment of a [=layout API formatting context=] +for a given |box|, |childBoxes|, |internalLayoutConstraints|, and an optional |internalBreakToken| +it must run the following steps: - 5. Let |documentDefinition| be the result of get a document layout definition given - |name|. + 1. Let |layoutFunction| be the ''layout()'' for the [=computed value=] of <> for + |box|. - If get a document layout definition returned failure, or if |documentDefinition| is - "invalid", then let |box| fallback to the flow layout and abort all - these steps. + 2. Let |name| be the first argument of the |layoutFunction|. - 6. Let |workletGlobalScope| be a {{LayoutWorkletGlobalScope}} from the list of worklet's - WorkletGlobalScopes from the layout {{Worklet}}. + 3. Let |documentDefinition| be the result of [=get a document layout definition=] given |name|. - The user agent must have, and select from at least two - {{LayoutWorkletGlobalScope}}s in the worklet's WorkletGlobalScopes list, - unless the user agent is under memory constraints. + If [=get a document layout definition=] returned failure, or if |documentDefinition| is + "invalid", then let |box| fallback to the [=flow layout=] and abort all these + steps. - Note: This is to ensure that authers do not rely on being able to store state on the global - object or non-regeneratable state on the class. + 4. Let |workletGlobalScope| be a {{LayoutWorkletGlobalScope}} from the list of [=worklet's + WorkletGlobalScopes=] from the layout {{Worklet}}, following the rules defined in + [[#global-scope-selection]]. - The user agent may also create a WorkletGlobalScope at this time, given the + The user agent may also [=create a WorkletGlobalScope=] at this time, given the layout {{Worklet}}. - 7. Run invoke a layout callback given |name|, |box|, |childBoxes|, - |internalLayoutConstraints|, |internalBreakToken|, and |workletGlobalScope| optionally in - parallel. + 5. Run [=invoke a layout callback=] given |name|, |box|, |childBoxes|, + |internalLayoutConstraints|, |internalBreakToken|, and |workletGlobalScope| optionally [=in + parallel=]. - Note: If the user agent runs invoke a intrinsic sizes callback on a thread in - parallel, it should select a layout worklet global scope which can be used on that - thread. + Note: If the user agent runs [=invoke a layout callback=] on a thread [=in parallel=], it + should select a layout worklet global scope which can be used on that thread.
@@ -1869,126 +1909,146 @@ When the user agent wants to invoke a layout callback given |name|, | |internalLayoutConstraints|, |internalBreakToken|, and |workletGlobalScope|, it must run the following steps: - 1. Let |definition| be the result of get a layout definition given |name|, and + 1. Let |definition| be the result of [=get a layout definition=] given |name|, and |workletGlobalScope|. - If get a layout definition returned failure, let the |box| fallback to the flow - layout and abort all these steps. + If [=get a layout definition=] returned failure, let the |box| fallback to the [=flow + layout=] and abort all these steps. - 2. Let |layoutInstance| be the result of get a layout class instance given |box|, + 2. Let |layoutInstance| be the result of [=get a layout class instance=] given |box|, |definition|, |workletGlobalScope|. - If get a layout class instance returned failure, let the |box| fallback to the - flow layout and abort all these steps. + If [=get a layout class instance=] returned failure, let the |box| fallback to the [=flow + layout=] and abort all these steps. + + 3. Let |context| be the result of [=create a layout API context=] given "layout". - 3. Let |sizingMode| be |definition|'s layout options' + 4. Let |sizingMode| be |definition|'s layout options' {{LayoutOptions/sizing}} property. - 4. Let |inputProperties| be |definition|'s input properties. + 5. Let |inputProperties| be |definition|'s input properties. - 5. Let |children| be a new list. + 6. Let |children| be a new [=list=]. - 6. For each |childBox| in |childBoxes| perform the following substeps: + 7. For each |childBox| in |childBoxes| perform the following substeps: - 1. Let |layoutChild| be the result of get a layout child given |workletGlobalScope|, - |name|, and |childBox|. + 1. Let |layoutChild| be the result of [=get a layout child=] given |workletGlobalScope|, + |name|, |childBox|, and |context|'s [=unique id=]. 2. Append |layoutChild| to |children|. - 7. Let |edges| be a new {{LayoutEdgeSizes}} populated with the computed value for all the - box model edges for |box|. + 8. Let |edges| be a new {{LayoutEdges}} populated with the [=computed value=] for all the [=box + model edges=] for |box|. - 8. Let |layoutConstraints| be the result of create a layout constraints object given + 9. Let |layoutConstraints| be the result of [=create a layout constraints object=] given |internalLayoutConstraints|, |box|, and |sizingMode|. - 9. Let |styleMap| be a new {{StylePropertyMapReadOnly}} populated with only the - computed values for properties listed in |inputProperties| for |box|. + 10. Let |styleMap| be the result of [=get a style map=] given |box|, and |inputProperties|. - Issue: We may want to store |styleMap| on |box| instead, similar to |layoutInstance|. - - 10. Let |breakToken| be a new {{BreakToken}} populated with the appropriate information from + 11. Let |breakToken| be a new {{BreakToken}} populated with the appropriate information from |internalBreakToken|. If |internalBreakToken| is null, let |breakToken| be null. - 11. At this stage the user agent may re-use a fragment from a previous invocation if + 12. At this stage the user agent may re-use a [=fragment=] from a previous invocation if |children|, |styleMap|, |layoutConstraints|, |breakToken| are equivalent to that previous invocation. If so let the fragment output be that cached fragment and abort all these steps. - 12. Let |layoutGeneratorFunction| be |definition|'s layout generator function. + 13. Let |layoutFunction| be |definition|'s [=layout function=]. - 13. Let |layoutGenerator| be the result of Invoke(|layoutGeneratorFunction|, - |layoutInstance|, «|children|, |edges|, |layoutConstraints|, |styleMap|, |breakToken|»). + 14. Let |value| be the result of [=Invoke=](|layoutFunction|, |layoutInstance|, «|children|, + |edges|, |layoutConstraints|, |styleMap|, |breakToken|»). - If an exception is thrown the let |box| fallback to the flow layout and abort - all these steps. + If an exception is [=thrown=] the let |box| fallback to the [=flow layout=] and abort all + these steps. + + 15. If |value| is a promise: - 14. Let |fragmentValue| be the result of run a generator given |layoutGenerator|, and - "layout". + - Then: - If run a generator returned failure, then let |box| fallback to the flow - layout and abort all these steps. + 1. Let |fragmentResultValue| be the result of [=run a work queue=] given |value|. - 15. Let |fragment| be the result of converting |fragmentValue| to a - {{FragmentResultOptions}}. If an exception is thrown, let |box| fallback to the - flow layout and abort all these steps. + If [=run a work queue=] returns failure, let the |box| fallback to the [=flow + layout=] and abort all these steps. - 16. For each |childFragment| in |fragment|'s - {{FragmentResultOptions/childFragments}}, perform the following stubsteps: + - Otherwise: - 1. If |childFragment|'s {{[[generator]]}} internal slot is not equal to |layoutGenerator|, - then let |box| fallback to the flow layout and abort all these steps. + 1. Let |fragmentResultValue| be |value|. - 17. If |sizingMode| is "block-like": + 16. If |fragmentResultValue| is a [=platform object=]: - Then: - 1. Let |inlineSize| be |layoutConstraints|' {{LayoutConstraints/fixedInlineSize}}. (This - value must be set if we are using "block-like" sizing). + 1. Let |fragmentResult| be the result [=converting=] |fragmentResultValue| to a + {{FragmentResult}}. - 2. Let |blockSize| be the result of calculating |box|'s border-box - block size (relative to |box|'s writing mode) exactly like block containers - do, given |fragment|'s {{FragmentResultOptions/autoBlockSize}} as the "intrinsic - height". + If an exception is [=thrown=], let |box| fallback to the [=flow layout=] and abort all + these steps. - - Otherwise (|sizingMode| is "manual"): + - Otherwise: - 1. Let |inlineSize| be |fragment|'s {{FragmentResultOptions/inlineSize}}. + 1. Let |fragmentResultOptions| be the result of [=converting=] |fragmentResultValue| to + a {{FragmentResultOptions}}. - 2. Let |blockSize| be |fragment|'s {{FragmentResultOptions/blockSize}}. + If an exception is [=thrown=], let |box| fallback to the [=flow layout=] and abort + all these steps. + + 2. Let |fragmentResult| be the result of [=construct a fragment result=] given + |fragmentResultOptions|. + + If an exception is [=thrown=], let |box| fallback to the [=flow layout=] and abort + all these steps. + + 17. Return an internal representation of a [=fragment=] with: - 18. Return a fragment for |box| with: + - The [=inline size=] set to |fragmentResult|'s {{FragmentResult/[[inline size]]}}. - - The inline size set to |inlineSize|. + - The [=block size=] set to |fragmentResult|'s {{FragmentResult/[[inline size]]}}. - - The block size set to |blockSize|. + - The child fragments set to |fragmentResult|'s {{FragmentResult/[[child fragments]]}}. - - The child fragments set to |fragment|'s {{FragmentResultOptions/childFragments}} - list. The ordering is important as this dictates their paint order - (described in [[#layout-api-containers]]). Their position relative to the border - box of the |fragment| should be based off the author specified - {{LayoutFragment/inlineOffset}} and {{LayoutFragment/blockOffset}}. + The ordering is important as this dictates their paint order (described in + [[#layout-api-containers]]). Their position relative to the border box of the + fragment should be based off the author specified {{LayoutFragment/inlineOffset}} and + {{LayoutFragment/blockOffset}}. - - The fragmentation break information set to |fragment|'s - {{FragmentResultOptions/breakToken}}. + - The [=fragmentation break=] information set to |fragmentResult|'s + {{FragmentResult/[[internal break token]]}}. - - Let |clonedData| be the result of invoking StructuredSerializeForStorage on - |fragment|'s {{FragmentResultOptions/data}}. + - Store |fragmentResult|'s {{FragmentResult/[[data]]}} with the [=fragment=]. - The user agent must store |clonedData| with the fragment.
+### Global Scope Selection ### {#global-scope-selection} + +When the user agent needs to select a {{LayoutWorkletGlobalScope}} from the layout [=worklet's +WorkletGlobalScopes=] [=list=] it must: + + - Select from at least two {{LayoutWorkletGlobalScope}}s, unless the user agent is + under memory constraints. + + - Not re-use the same {{LayoutWorkletGlobalScope}} more than 1000 times in a row. + + Note: The 1000 limit was picked as a high upper bound, this limit may improve (downwards) + over time. + +Note: These rules exist to ensure that authors do not rely on being able to store state on the + global object or non-regeneratable state on the class. See [[worklets-1#code-idempotency]]. + ### Utility Algorithms ### {#utility-algorithms} -The section specifies algorithms common to the determine the intrinsic sizes and generate -a fragment algorithms. +The section specifies algorithms common to the [=determine the intrinsic sizes=] and [=generate +a fragment=] algorithms. + +Note: [=Get a document layout definition=] returns a [=document layout definition=] from the + owning [=document=].
When the user agent wants to get a document layout definition given |name|, it must run the following steps: - 1. Let |documentLayoutDefinitionMap| be the associated document's document layout - definitions map. + 1. Let |documentLayoutDefinitionMap| be the associated [=document's=] [=document layout + definitions=] map. 2. If |documentLayoutDefinitionMap|[|name|] does not exist, return failure and abort all these steps. @@ -1996,18 +2056,22 @@ When the user agent wants to get a document layout definition given | 3. Return the result of get |documentLayoutDefinitionMap|[|name|].
+Note: [=Get a layout definition=] returns a [=layout definition=] for a given + {{LayoutWorkletGlobalScope}}, it the desired definition doesn't exist it will "invalidate" the + [=document layout definition=], (so that the layout can't be used again), and return failure. +
When the user agent wants to get a layout definition given |name|, and |workletGlobalScope|, it must run the following steps: - 1. Let |layoutDefinitionMap| be |workletGlobalScope|'s layout definitions map. + 1. Let |layoutDefinitionMap| be |workletGlobalScope|'s [=layout definitions=] map. 2. If |layoutDefinitionMap|[|name|] does not exist, run the following steps: - 1. Queue a task to run the following steps: + 1. [=Queue a task=] to run the following steps: - 1. Let |documentLayoutDefinitionMap| be the associated document's document - layout definition map. + 1. Let |documentLayoutDefinitionMap| be the associated [=document's=] [=document layout + definition=] map. 2. Set |documentLayoutDefinitionMap|[|name|] to "invalid". @@ -2016,160 +2080,380 @@ When the user agent wants to get a layout definition given |name|, an 2. Return failure, and abort all these steps. - 3. Return the result of get |layoutDefinitionMap|[|name|]. + 3. Return the result of [=get=] |layoutDefinitionMap|[|name|].
+Note: [=Get a layout class instance=] returns an instance of the web developer provided class. + (Registered in {{registerLayout()}}). If one isn't present yet, it will create a new one. This + algorithm may fail, as the constructor may throw an exception. +
When the user agent wants to get a layout class instance given |box|, |definition|, and |workletGlobalScope|, it must run the following steps: - 1. Let |layoutClassInstanceMap| be |box|'s layout class instances map. + 1. Let |layoutClassInstanceMap| be |box|'s [=layout class instances=] map. - 2. Let |layoutInstance| be the result of get - |layoutClassInstanceMap|[|workletGlobalScope|]. If |layoutInstance| is null, run the - following steps: + 2. Let |layoutInstance| be the result of [=get=] |layoutClassInstanceMap|[|workletGlobalScope|]. + If |layoutInstance| is null, run the following steps: - 1. If the constructor valid flag on |definition| is false, then return failure and + 1. If the [=constructor valid flag=] on |definition| is false, then return failure and abort all these steps. - 2. Let |layoutCtor| be the class constructor on |definition|. + 2. Let |layoutCtor| be the [=class constructor=] on |definition|. - 3. Let |layoutInstance| be the result of Construct(|layoutCtor|). + 3. Let |layoutInstance| be the result of [=Construct=](|layoutCtor|). - If construct throws an exception, set the |definition|'s constructor valid - flag to false, then return failure and abort all these steps. + If [=construct=] throws an exception, set the |definition|'s [=constructor valid flag=] + to false, then return failure and abort all these steps. 4. Set |layoutClassInstanceMap|[|workletGlobalScope|] to |layoutInstance|. 4. Return |layoutInstance|.
-
-When the user agent wants to run a generator given |generator|, and |generatorType|, it +
+When the user agent wants to get a style map given |box|, and |inputProperties|, it +must run the following steps: + + 1. If |box|'s [=styleMap=] is null, then: + + 1. Let |styleMap| be a new {{StylePropertyMapReadOnly}} populated with only the + [=computed values=] for properties listed in |inputProperties| for |box|. + + 2. Set |box|'s [=styleMap=] internal slot to |styleMap|. + + 2. Return |box|'s {{StylePropertyMapReadOnly}} contained in the [=styleMap=] internal slot. + +
+ +[=Run a work queue=] is designed to allow user agents to work in both a single threaded, and +multi-threaded environment. + +Note: [=Run a work queue=] processes [=layout api work task=]s enqueued with + {{LayoutChild/intrinsicSizes()}} and {{LayoutChild/layoutNextFragment()}}. It will continue + processing tasks until the promise from the web developers layout or intrinsicSizes method is + resolved, or the queue is empty after running the microtask queue. + + The result of running the queue will either be the result of the layout or intrinsicSizes + method, or failure. + +
+When the user agent wants to run a work queue given |promise|, and |workQueue|, it must run the following steps: - 1. Let |done| be a boolean initialized to false. + 1. If |promise| is not a promise, return failure. + + 2. [=While=] |workQueue| is not [=list/empty=], and |promise| is pending: + + 1. [=list/For each=] |task| in |workQueue|: + + 1. Let |layoutChild| be |task|'s [=layout api work task/layout child=]. + + 2. Let |box| be |layoutChild|'s [=box=] in the {{LayoutChild/[[box]]}} internal slot. + + 3. Let |childPromise| be |task|'s [=layout api work task/promise=]. + + 2. If |task|'s [=layout api work task/task type=] is "layout", + + - Then [=queue a task=], or run synchronously, the following substeps: + + 1. Let |childConstraints| be |task|'s [=layout api work task/layout constraints=]. + + 2. Let |childBreakToken| be |task|'s [=layout api work task/child break token=]. + + 3. Let |targetRealm| be |layoutChild|'s [=Realm=]. + + 4. Let |internalFragment| be the result of the user agent producing a + [=fragment=] based on |box|, |childConstraints|, and |childBreakToken|. + + Invoking [=translate a LayoutConstraintsOptions to internal constraints=] + given |childConstraints|, must be run to translate the given + {{LayoutConstraintsOptions}} into the internal constraints for the user + agent's layout engine. + + 5. Let |fragment| be a new {{LayoutFragment}} with: + + - {{LayoutFragment/inlineSize}} being |internalFragment|'s [=inline size=] + relative to the [=current layout's=] writing mode. + + - {{LayoutFragment/blockSize}} being |internalFragment|'s [=block size=] + relative to the [=current layout's=] writing mode. + + - {{LayoutFragment/inlineOffset}} initially set to 0. + + - {{LayoutFragment/blockOffset}} initially set to 0. + + - {{LayoutFragment/breakToken}} being a new {{ChildBreakToken}} representing + |internalFragment|'s internal break token, if any. + + - If |internalFragment| has a |clonedData| object stored with it, let + {{LayoutFragment/data}} being the result of + [=StructuredDeserialize=](|clonedData|, |targetRealm|), otherwise null. + + 6. Resolve |childPromise| with |fragment|. - 2. Let |nextValue| be undefined. + - Otherwise [=queue a task=], or run synchronously, the following substeps: - 3. Perform the following substeps until |done| is true: + 1. Let |internalIntrinsicSizes| be the result of the user agent calculating the + border box min/max content contribution of |box|. - 1. Let |nextFunction| be the result of Get(|generator|, "next"). + 2. Let |intrinsicSizes| be a new {{IntrinsicSizes}} with: - 2. If the result of IsCallable(|nextFunction|) is false, throw a - TypeError and abort all these steps. + - {{IntrinsicSizes/minContentSize}} being |internalIntrinsicSizes|' + border box min-content contribution, relative to the [=current + layout's=] writing mode. - 3. Let |nextResult| be the result of calling Invoke(|nextFunction|, |generator|, - «|nextValue|»). + - {{IntrinsicSizes/maxContentSize}} being |internalIntrinsicSizes|'s + border box max-content contribution, relative to the [=current + layout's=] writing mode. - If an exception is thrown return failure, and abort all these steps. + 3. Resolve |childPromise| with |intrinsicSizes|. - 4. If the result of Type(|nextResult|) is not Object, throw a TypeError - and abort all these steps. + 2. Wait (optionally [=in parallel=]) for all of the above tasks to complete. - 5. Let |requestOrRequests| be the result of Get(nextResult|, "value"). + Note: If the tasks were perform synchronously, then this step is a no-op. - 6. Let |done| be the result of Get(|nextResult|, "done"). + 3. [=list/Empty=] |workQueue|. - 7. If the result of GetMethod(|requestOrRequests|, @@iterable) exists - then: + 4. [=Perform a microtask checkpoint=]. + + 3. If |promise| isn't fulfilled (it is pending, or got rejected), return failure. + + 4. Return the fulfilled value of |promise|. + +
+ +Examples {#examples} +==================== + +
+The layout algorithm below performs a block-like layout (positioning fragments sequentially in the +block direction), while centering its children in the inline direction. + +
+registerLayout('block-like', class {
+    async intrinsicSizes(children, edges, styleMap) {
+      const childrenSizes = await Promise.all(children.map((child) => {
+          return child.intrinsicSizes();
+      }));
+
+      const maxContentSize = childrenSizes.reduce((max, childSizes) => {
+          return Math.max(max, childSizes.maxContentSize);
+      }, 0) + edges.inline;
+
+      const minContentSize = childrenSizes.reduce((max, childSizes) => {
+          return Math.max(max, childSizes.minContentSize);
+      }, 0) + edges.inline;
+
+      return {maxContentSize, minContentSize};
+    }
+
+    async layout(children, edges, constraints, styleMap) {
+        // Determine our (inner) available size.
+        const availableInlineSize = constraints.fixedInlineSize - edges.inline;
+        const availableBlockSize = constraints.fixedBlockSize ?
+            constraints.fixedBlockSize - edges.block : null;
+
+        const childFragments = [];
+        const childConstraints = { availableInlineSize, availableBlockSize };
+
+        const childFragments = await Promise.all(children.map((child) => {
+            return child.layoutNextFragment(childConstraints);
+        }));
+
+        let blockOffset = edges.blockStart;
+        for (let fragment of childFragments) {
+            // Position the fragment in a block like manner, centering it in the
+            // inline direction.
+            fragment.blockOffset = blockOffset;
+            fragment.inlineOffset = Math.max(
+                edges.inlineStart,
+                (availableInlineSize - fragment.inlineSize) / 2);
 
-            1. Set |results| to be a new empty list.
+            blockOffset += fragment.blockSize;
+        }
 
-            2. Let |requests| be the result of converting |requestOrRequests| to a
-                sequence<LayoutFragmentRequestOrIntrinsicSizesRequest>.
+        const autoBlockSize = blockOffset + edges.blockEnd;
 
-                If an exception is thrown, rethrow the exception and abort all these steps.
+        return {
+            autoBlockSize,
+            childFragments,
+        };
+    }
+});
+
+
- 3. For each |request| in |requests| perform the following substeps: +
+The layout algorithm performs a flexbox-like distribution of spare space in the inline direction. It +creates child layout constraints which specify that a child should be a fixed inline size. - 1. Let |result| be the result of produce a generator result given |request|, - |generator|, |generatorType|. +
+registerLayout('flex-distribution-like', class {
+    async intrinsicSizes(children, edges, styleMap) {
+      const childrenSizes = await Promise.all(children.map((child) => {
+          return child.intrinsicSizes();
+      }));
 
-                2. Append |result| to |results|.
+      const maxContentSize = childrenSizes.reduce((sum, childSizes) => {
+          return sum + childSizes.maxContentSize;
+      }, 0) + edges.inline;
 
-            4. Set |nextValue| to be |results|.
+      const minContentSize = childrenSizes.reduce((max, childSizes) => {
+          return sum + childSizes.minContentSize;
+      }, 0) + edges.inline;
 
-            5. Continue.
+      return {maxContentSize, minContentSize};
+    }
 
-        8. Let |request| be the result of converting |requestOrRequests| to a
-            {{LayoutFragmentRequestOrIntrinsicSizesRequest}}.
+    async layout(children, edges, constraints, styleMap) {
+        // Determine our (inner) available size.
+        const availableInlineSize =
+            constraints.fixedInlineSize - edges.inline;
+        const availableBlockSize = constraints.fixedBlockSize ?
+            constraints.fixedBlockSize - edges.block : null;
 
-            If an exception is thrown, rethrow the exception and abort all these steps.
+        const childConstraints = { availableInlineSize, availableBlockSize };
 
-        9. Let |result| be the result of produce a generator result given |request|,
-            |generator|, |generatorType|.
+        const unconstrainedChildFragments = await Promise.all(children.map((child) => {
+            return child.layoutNextFragment(childConstraints);
+        }));
 
-            If produce a generator result returns failure, return failure, and abort all
-            these steps.
+        const unconstrainedSizes = [];
+        const totalSize = unconstrainedChildFragments.reduce((sum, fragment, i) => {
+            unconstrainedSizes[i] = fragment.inlineSize;
+            return sum + fragment.inlineSize;
+        }, 0);
 
-        10. Set |nextValue| to be |result|.
+        // Distribute spare space between children.
+        const remainingSpace = Math.max(0, inlineSize - totalSize);
+        const extraSpace = remainingSpace / children.length;
 
-        11. Continue.
+        const childFragments = await Promise.all(children.map((child, i) => {
+            return child.layoutNextFragment({
+                fixedInlineSize: unconstrainedSizes[i] + extraSpace,
+                availableBlockSize
+            });
+        }));
 
-        The user agent may perform the above loop out of order, and in parallel. The ordering
-        for |requests| and |results| however must be consistent.
+        // Position the fragments.
+        let inlineOffset = 0;
+        let maxChildBlockSize = 0;
+        for (let fragment of childFragments) {
+            fragment.inlineOffset = inlineOffset;
+            fragment.blockOffset = edges.blockStart;
 
-        Note: This is to allow user agents to run the appropriate layout algorithm on a different
-            thread, or asynchronously (e.g. time slicing layout work with other work). If the user
-            agent performs the loop in parallel, the outside loop has to wait until all the cross
-            thread tasks are complete before calling the generator again. It cannot return partial
-            results to the author.
+            inlineOffset += fragment.inlineSize;
+            maxChildBlockSize = Math.max(maxChildBlockSize, fragment.blockSize);
+        }
 
-    3. Return the result of calling Get(|nextResult|, "value").
+        return {
+            autoBlockSize: maxChildBlockSize + edges.block,
+            childFragments,
+        };
+    }
+});
+
-
-When the user agent wants to produce a generator result given |request|, |generator|, and -|generatorType|, it must run the following steps: - 1. If |request| is a {{IntrinsicSizesRequest}} then: +
+This example shows a simple layout which indents child fragments for a certain number of +lines. + +This example also demonstrates using the previous {{LayoutFragment/breakToken}} of a +{{LayoutFragment}} to produce the next fragment for the {{LayoutChild}}. + +It also demonstrates using the {{BreakToken}} to respect the {{LayoutConstraints}}' +{{LayoutConstraints/blockFragmentationType}}, it resumes it layout from the previous {{BreakToken}}. +It returns a {{FragmentResultOptions}} with a {{FragmentResultOptions/breakToken}} which is used to +resume the layout. + +
+registerLayout('indent-lines', class {
+    static layoutOptions = {childDisplay: 'normal'};
+    static inputProperties = ['--indent', '--indent-lines'];
 
-        1. Let |layoutChild| be the result of looking up the internal slot
-            {{IntrinsicSizesRequest/[[layoutChild]]}} on |request|.
+    async layout(children, edges, constraints, styleMap, breakToken) {
+        // Determine our (inner) available size.
+        const availableInlineSize =
+            constraints.fixedInlineSize - edges.inline;
+        const availableBlockSize = constraints.fixedBlockSize ?
+            constraints.fixedBlockSize - edges.block : null;
 
-        2. Let |box| be the result of looking up the internal slot {{LayoutChild/[[box]]}} on
-            |layoutChild|.
+        // Detrermine the number of lines to indent, and the indent amount.
+        const indent = resolveLength(constraints, styleMap.get('--indent'));
+        let lines = styleMap.get('--indent-lines').value;
 
-        3. If |box| is not attached to the box tree, return failure and abort all
-            these steps.
+        const childFragments = [];
 
-            Note: The author may hold onto a {{LayoutChild}} from a previous invocation. It is
-                assumed that a box is only ever attached to the box-tree once, and not re-used.
+        let childBreakToken = null;
+        if (breakToken) {
+            childBreakToken = breakToken.childBreakTokens[0];
 
-        4. Let |internalIntrinsicSizes| be the result of the user agent calculating the border
-            box min/max content contribution of |box|.
+            // Remove all the children we have already produced fragments for.
+            children.splice(0, children.indexOf(childBreakToken.child));
+        }
 
-        5. Return the result of create an intrinsic sizes object given |request|, and
-            |internalIntrinsicSizes|.
+        let blockOffset = edges.blockStart;
+        let child = children.shift();
+        while (child) {
+            const shouldIndent = lines-- > 0;
 
-    2. If |request| is a {{LayoutFragmentRequest}} and |generatorType| is "layout"
-        then:
+            // Adjust the inline size for the indent.
+            const childAvailableInlineSize = shouldIndent ?
+                availableInlineSize - indent : availableInlineSize;
 
-        1. Let |layoutChild| be result of looking up the internal slot
-            {{LayoutFragmentRequest/[[layoutChild]]}} on |request|.
+            const childConstraints = {
+                availableInlineSize: childAvailableInlineSize,
+                availableBlockSize,
+                percentageInlineSize: availableInlineSize,
+                blockFragmentationType: constraints.blockFragmentationType,
+            };
 
-        2. Let |box| be the result of looking up the internal slot {{LayoutChild/[[box]]}} on
-            |layoutChild|.
+            const fragment = await child.layoutNextFragment(childConstraints,
+                                                            childBreakToken);
+            childFragments.push(fragment);
 
-        3. If |box| is not attached to the box tree, return failure and abort all
-            these steps.
+            // Position the fragment.
+            fragment.inlineOffset = shouldIndent ?
+                edges.inlineStart + indent : edges.inlineStart;
+            fragment.blockOffset = blockOffset;
+            blockOffset += fragment.blockSize;
 
-            Note: The author may hold onto a {{LayoutChild}} from a previous invocation. It is
-                assumed that a box is only ever attached to the box-tree once, and not re-used.
+            // Check if we have gone over the block fragmentation limit.
+            if (constraints.blockFragmentationType != 'none' &&
+                blockOffset > constraints.blockSize) {
+                break;
+            }
 
-        4. Let |childLayoutConstraints| be the result of looking up the internal slot
-            {{LayoutFragmentRequest/[[layoutConstraints]]}} on |request|.
+            if (fragment.breakToken) {
+                childBreakToken = fragment.breakToken;
+            } else {
+                // If a fragment doesn't have a break token, we move onto the
+                // next child.
+                child = children.shift();
+                childBreakToken = null;
+            }
+        }
 
-        5. Let |childBreakToken| be the result of looking up the internal slot
-            {{LayoutFragmentRequest/[[breakToken]]}} on |request|.
+        const autoBlockSize = blockOffset + edges.blockEnd;
 
-        6. Let |internalFragment| be the result of the user agent producing a fragment based
-            on |box|, |childLayoutConstraints|, and |childBreakToken|.
+        // Return our fragment.
+        const result = {
+            autoBlockSize,
+            childFragments: childFragments,
+        }
 
-        7. Return the result of create a layout fragment given |generator|, |request|, and
-            |internalFragment|.
+        if (childBreakToken) {
+            result.breakToken = {
+                childBreakTokens: [childBreakToken],
+            };
+        }
 
-    3. Return failure (neither of the above branches was taken).
+        return result;
+    }
+});
+
Security Considerations {#security-considerations} diff --git a/css-layout-api/img/edges.png b/css-layout-api/img/edges.png new file mode 100644 index 00000000..6d01d1fa Binary files /dev/null and b/css-layout-api/img/edges.png differ diff --git a/css-layout-api/img/layout-fragment-offsets.png b/css-layout-api/img/layout-fragment-offsets.png new file mode 100644 index 00000000..c42a15df Binary files /dev/null and b/css-layout-api/img/layout-fragment-offsets.png differ diff --git a/css-paint-api/EXPLAINER.md b/css-paint-api/EXPLAINER.md index f01f1e56..96a5402c 100644 --- a/css-paint-api/EXPLAINER.md +++ b/css-paint-api/EXPLAINER.md @@ -73,7 +73,7 @@ if ('paintWorklet' in CSS) { } ``` -See the worklets explainer for a more involed explaination of worklets. In short worklets: +See the worklets explainer for a more involved explaination of worklets. In short worklets: - Are similar to workers in that the script runs in a separate global script context. - A script inside a worklet has no DOM, Network, Database, etc access. - The global script context lifetime is not defined (you should expect the script context to be killed at any point). diff --git a/css-paint-api/Overview.bs b/css-paint-api/Overview.bs index c5945919..f7570b54 100644 --- a/css-paint-api/Overview.bs +++ b/css-paint-api/Overview.bs @@ -34,29 +34,14 @@ Ignored Terms: PaintWorklet
-urlPrefix: https://heycam.github.io/webidl/; type: dfn;
-    text: InvalidModificationError
-    urlPrefix: #common-;
-        text: Function
-    urlPrefix: #dfn-;
-        url: throw; text: thrown
-    urlPrefix: #idl-;
-        text: boolean
-        text: DOMException
-    url: es-type-mapping; text: converting
-urlPrefix: https://html.spec.whatwg.org/multipage/; type: dfn;
-    urlPrefix: scripting.html
-        text: reset the rendering context to its default state
-        text: output bitmap
-        text: set bitmap dimensions
-        url: concept-canvas-alpha; text: alpha
 urlPrefix: https://tc39.github.io/ecma262/#sec-; type: dfn;
     text: constructor
     text: Construct
@@ -87,10 +72,12 @@ Paint Worklet {#paint-worklet}
 The {{paintWorklet}} attribute allows access to the {{Worklet}} responsible for all the classes
 which are related to painting.
 
-The {{paintWorklet}}'s worklet global scope type is {{PaintWorkletGlobalScope}}.
+The {{paintWorklet}}'s [=worklet global scope type=] is {{PaintWorkletGlobalScope}}.
+
+The {{paintWorklet}}'s worklet destination type is "paintworklet".
 
 
-partial interface CSS {
+partial namespace CSS {
     [SameObject] readonly attribute Worklet paintWorklet;
 };
 
@@ -103,7 +90,7 @@ identical to the Window.{{Window/devicePixelRatio}} property.
 [Global=(Worklet,PaintWorklet),Exposed=PaintWorklet]
 interface PaintWorkletGlobalScope : WorkletGlobalScope {
-    void registerPaint(DOMString name, VoidFunction paintCtor);
+    undefined registerPaint(DOMString name, VoidFunction paintCtor);
     readonly attribute unrestricted double devicePixelRatio;
 };
 
@@ -136,67 +123,70 @@ dictionary PaintRenderingContext2DSettings { Concepts {#concepts} ==================== -A paint definition is a struct which describes the information needed by the +A paint definition is a [=struct=] which describes the information needed by the {{PaintWorkletGlobalScope}} about the author defined <> (which can be referenced by the <> function). It consists of: - - class constructor which is the class constructor. + - class constructor which is the class [=constructor=]. - - paint function which is the paint Function - callback function type. + - paint function which is the paint [=Function=] + [=callback function=] type. - constructor valid flag. - - input properties which is a list of + - input properties which is a [=list=] of DOMStrings. - A PaintRenderingContext2DSettings object. -A document paint definition is a struct which describes the information -needed by the document about the author defined <> function (which can be referenced +A document paint definition is a [=struct=] which describes the information +needed by the [=document=] about the author defined <> function (which can be referenced by the paint function). It consists of: - - A input properties which is a list of + - A input properties which is a [=list=] of DOMStrings. - - A input argument syntaxes which is a list of - parsed [[css-properties-values-api-1#supported-syntax-strings]]. + - A input argument syntaxes which is a [=list=] of + [=syntax definitions=]. - A PaintRenderingContext2DSettings object. Registering Custom Paint {#registering-custom-paint} ==================================================== -The document has a map of document paint definitions. Initially +The [=document=] has a [=map=] of document paint definitions. Initially this map is empty; it is populated when {{registerPaint(name, paintCtor)}} is called. -A {{PaintWorkletGlobalScope}} has a map of paint definitions. Initially this map +A {{PaintWorkletGlobalScope}} has a [=map=] of paint definitions. Initially this map is empty; it is populated when {{registerPaint(name, paintCtor)}} is called. -A {{PaintWorkletGlobalScope}} has a map of paint class instances. Initially this -map is empty; it is populated when draw a paint image is invoked by the user agent. +A {{PaintWorkletGlobalScope}} has a [=map=] of paint class instances. Initially this +map is empty; it is populated when [=draw a paint image=] is invoked by the user agent. -Instances of paint classes in the paint class instances map may be disposed and removed from +Instances of paint classes in the [=paint class instances=] map may be disposed and removed from the map by the user agent at any time. This may be done when a <> function no longer is used, or the user agent needs to reclaim memory.
When the registerPaint(|name|, |paintCtor|) method is called, the user agent must run the following steps: - 1. If the |name| is an empty string, throw a TypeError and abort all these steps. + 1. If the |name| is an empty string, [=throw=] a [=TypeError=] and abort all these steps. - 2. Let |paintDefinitionMap| be {{PaintWorkletGlobalScope}}'s paint definitions map. + 2. Let |paintDefinitionMap| be {{PaintWorkletGlobalScope}}'s [=paint definitions=] map. - 3. If |paintDefinitionMap|[|name|] exists throw a - "InvalidModificationError" DOMException and abort all these steps. + 3. If |paintDefinitionMap|[|name|] [=map/exists=] [=throw=] a + "{{InvalidModificationError}}" {{DOMException}} and abort all these steps. 4. Let |inputProperties| be an empty sequence<DOMString>. - 5. Let |inputPropertiesIterable| be the result of Get(|paintCtor|, "inputProperties"). + 5. Let |inputPropertiesIterable| be the result of [=Get=](|paintCtor|, "inputProperties"). 6. If |inputPropertiesIterable| is not undefined, then set |inputProperties| to the result of - converting |inputPropertiesIterable| to a sequence<DOMString>. If an - exception is thrown, rethrow the exception and abort all these steps. + [=converting=] |inputPropertiesIterable| to a sequence<DOMString>. If an + exception is [=thrown=], rethrow the exception and abort all these steps. + + 7. Filter |inputProperties| so that it only contains [=supported CSS properties=] and + [=custom properties=]. Note: The list of CSS properties provided by the input properties getter can either be custom or native CSS properties. @@ -207,94 +197,94 @@ called, the user agent must run the following steps: also contains currently invalid properties for the user agent. For example margin-bikeshed-property. - 7. Let |inputArguments| be an empty sequence<DOMString>. + 8. Let |inputArguments| be an empty sequence<DOMString>. - 8. Let |inputArgumentsIterable| be the result of Get(|paintCtor|, "inputArguments"). + 9. Let |inputArgumentsIterable| be the result of [=Get=](|paintCtor|, "inputArguments"). - 9. If |inputArgumentsIterable| is not undefined, then set |inputArguments| to the result of - converting |inputArgumentsIterable| to a sequence<DOMString>. If an - execption is thrown, rethrow the execption and abort all these steps. + 10. If |inputArgumentsIterable| is not undefined, then set |inputArguments| to the result of + [=converting=] |inputArgumentsIterable| to a sequence<DOMString>. If an + exception is thrown, rethrow the exception and abort all these steps. - 10. Let |inputArgumentSyntaxes| be an empty list. + 11. Let |inputArgumentSyntaxes| be an [=list/empty=] [=list=]. - 11. For each |item| in |inputArguments| perform the following substeps: + 12. [=list/For each=] |item| in |inputArguments| perform the following substeps: - 1. Let |parsedSyntax| be the result of parsing |item| according to the rules in - [[css-properties-values-api-1#supported-syntax-strings]]. If it fails to parse - throw a TypeError and abort all these steps. + 1. Attempt to [=consume a syntax definition=] from |item|. + If failure was returned, [=throw=] a [=TypeError=] and abort all these steps. + Otherwise, let |parsedSyntax| be the returned [=syntax definition=]. - 2. Append |parsedSyntax| to |inputArgumentSyntaxes|. + 2. [=list/Append=] |parsedSyntax| to |inputArgumentSyntaxes|. - 12. Let |contextOptionsValue| be the result of Get(|paintCtor|, "contextOptions"). + 13. Let |contextOptionsValue| be the result of [=Get=](|paintCtor|, "contextOptions"). - 13. Let |paintRenderingContext2DSettings| be the result of converting + 14. Let |paintRenderingContext2DSettings| be the result of [=converting=] |contextOptionsValue| to a {{PaintRenderingContext2DSettings}}. - If an exception is thrown, rethrow the exception and abort all these steps. + If an exception is [=thrown=], rethrow the exception and abort all these steps. Note: Setting paintRenderingContext2DSettings.alpha is false allows user agents to anti-alias text in addition to performing "visibility" optimizations, e.g. not painting an image behind the paint image as the paint image is opaque. - 14. If the result of IsConstructor(|paintCtor|) is false, throw a TypeError + 15. If the result of [=IsConstructor=](|paintCtor|) is false, [=throw=] a [=TypeError=] and abort all these steps. - 15. Let |prototype| be the result of Get(|paintCtor|, "prototype"). + 16. Let |prototype| be the result of [=Get=](|paintCtor|, "prototype"). - 16. If the result of Type(|prototype|) is not Object, throw a TypeError and + 17. If the result of [=Type=](|prototype|) is not Object, [=throw=] a [=TypeError=] and abort all these steps. - 17. Let |paintValue| be the result of Get(|prototype|, "paint"). + 18. Let |paintValue| be the result of [=Get=](|prototype|, "paint"). - 18. Let |paint| be the result of converting |paintValue| to the Function - callback function type. Rethrow any exceptions from the conversion. + 19. Let |paint| be the result of [=converting=] |paintValue| to the [=Function=] + [=callback function=] type. Rethrow any exceptions from the conversion. - 19. Let |definition| be a new paint definition with: + 20. Let |definition| be a new [=paint definition=] with: - - class constructor being |paintCtor|. + - [=paint definition/class constructor=] being |paintCtor|. - - paint function being |paint|. + - [=paint function=] being |paint|. - - constructor valid flag being true. + - [=paint definition/constructor valid flag=] being true. - - input properties being |inputProperties|. + - [=paint definition/input properties=] being |inputProperties|. - - PaintRenderingContext2DSettings object being |paintRenderingContext2DSettings|. + - [=paint definition/PaintRenderingContext2DSettings object=] being |paintRenderingContext2DSettings|. - 20. Set |paintDefinitionMap|[|name|] to |definition|. + 21. [=map/Set=] |paintDefinitionMap|[|name|] to |definition|. - 21. Queue a task to run the following steps: + 22. [=Queue a task=] to run the following steps: - 1. Let |documentPaintDefinitionMap| be the associated document's document paint - definitions map. + 1. Let |documentPaintDefinitionMap| be the associated [=document's=] [=document paint + definitions=] [=map=]. - 2. Let |documentDefinition| be a new document paint definition with: + 2. Let |documentDefinition| be a new [=document paint definition=] with: - - input properties being |inputProperties|. + - [=document paint definition/input properties=] being |inputProperties|. - - input argument syntaxes being + - [=document paint definition/input argument syntaxes=] being |inputArgumentSyntaxes|. - - PaintRenderingContext2DSettings object being |paintRenderingContext2DSettings|. + - [=document paint definition/PaintRenderingContext2DSettings object=] being |paintRenderingContext2DSettings|. - 3. If |documentPaintDefinitionMap|[|name|] exists, run the following steps: + 3. If |documentPaintDefinitionMap|[|name|] [=map/exists=], run the following steps: - 1. Let |existingDocumentDefinition| be the result of get + 1. Let |existingDocumentDefinition| be the result of [=map/get=] |documentPaintDefinitionMap|[|name|]. 2. If |existingDocumentDefinition| is "invalid", abort all these steps. 3. If |existingDocumentDefinition| and |documentDefinition| are not equivalent, (that is - input properties, input argument syntaxes, and PaintRenderingContext2DSettings object are different), then: - Set |documentPaintDefinitionMap|[|name|] to "invalid". + [=map/Set=] |documentPaintDefinitionMap|[|name|] to "invalid". Log an error to the debugging console stating that the same class was registered with different inputProperties, inputArguments, or paintRenderingContext2DSettings. - 4. Otherwise, set |documentPaintDefinitionMap|[|name|] to + 4. Otherwise, [=map/set=] |documentPaintDefinitionMap|[|name|] to |documentDefinition|. Note: The list of input properties should only be looked up once, the class doesn't have the @@ -324,12 +314,12 @@ The <> function is an additional notation to be supported by the <
-For the 'cursor' property, the <> function should be treated as an invalid image and +For the 'cursor' property, the <> function should be treated as an [=invalid image=] and fallback to the next supported <>. -At computed value time the <> function does not need to match the grammar -registered by {{registerPaint()}}. Instead this will result in an invalid image when the -parsing occurs inside draw a paint image. +At [=computed value=] time the <> function does not need to match the grammar +registered by {{registerPaint()}}. Instead this will result in an [=invalid image=] when the +parsing occurs inside [=draw a paint image=]. The 2D rendering context {#2d-rendering-context} ================================================ @@ -355,19 +345,27 @@ Note: The {{PaintRenderingContext2D}} implements a subset of the {{CanvasRenderi Specifically it doesn't implement the {{CanvasImageData}}, {{CanvasUserInterface}}, {{CanvasText}}, or {{CanvasTextDrawingStyles}} APIs. -A {{PaintRenderingContext2D}} object has a output bitmap. This is initialised when the -object is created. The size of the output bitmap is the size of the fragment it is -rendering. +A {{PaintRenderingContext2D}} object has a output bitmap. +This is initialised when the object is created. +The size of the [=PaintRenderingContext2D/output bitmap=] is the [=concrete object size=] +of the object it is rendering to. + +A {{PaintRenderingContext2D}} object also has an alpha flag, +which can be set to true or false. +Initially, when the context is created, +its alpha flag must be set to true. +When a {{PaintRenderingContext2D}} object has its alpha flag set to false, +then its alpha channel must be fixed to 1.0 (fully opaque) for all pixels, +and attempts to change the alpha component of any pixel must be silently ignored. -The size of the output bitmap does not necessarily represent the size of the actual bitmap +The size of the [=PaintRenderingContext2D/output bitmap=] does not necessarily represent the size of the actual bitmap that the user agent will use internally or during rendering. For example, if the visual viewport is zoomed the user agent may internally use bitmaps which correspond to the number of device pixels in the coordinate space, so that the resulting rendering is of high quality. Additionally the user agent may record the sequence of drawing operations which have been applied to -the output bitmap such that the user agent can subsequently draw onto a device bitmap at the -correct resolution. This also allows user agents to re-use the same output of the output -bitmap repeatably while the visual viewport is being zoomed for example. +the [=PaintRenderingContext2D/output bitmap=] such that the user agent can subsequently draw onto a device bitmap at the +correct resolution. This also allows user agents to re-use the same output of the [=PaintRenderingContext2D/output bitmap=] repeatably while the visual viewport is being zoomed for example. Whenever "currentColor" is used as a color in the {{PaintRenderingContext2D}} API, it is treated as opaque black. @@ -388,13 +386,13 @@ is treated as opaque black. When the user agent is to create a PaintRenderingContext2D object for a given |width|, |height|, and |paintRenderingContext2DSettings|, it must run the following steps: 1. Create a new {{PaintRenderingContext2D}}. - 2. Set bitmap dimensions for the context's output bitmap to the rounded values of |width| and |height|. - 3. Set the {{PaintRenderingContext2D}}'s alpha flag to |paintRenderingContext2DSettings|'s {{alpha}}. + 2. [=Set bitmap dimensions=] for the context's [=PaintRenderingContext2D/output bitmap=] to the rounded values of |width| and |height|. + 3. Set the {{PaintRenderingContext2D}}'s [=PaintRenderingContext2D/alpha=] flag to |paintRenderingContext2DSettings|'s {{alpha}}. 4. Return the new {{PaintRenderingContext2D}}. -Note: The initial state of the rendering context is set inside the set bitmap dimensions - algorithm, as it invokes reset the rendering context to its default state and clears the - output bitmap. +Note: The initial state of the rendering context is set inside the [=set bitmap dimensions=] + algorithm, as it invokes [=reset the rendering context to its default state=] and clears the + [=PaintRenderingContext2D/output bitmap=].
Drawing a CSSImageValue {#drawing-a-cssimagevalue} @@ -409,14 +407,15 @@ For interfaces which use the {{CanvasDrawImage}} mixin: {{CanvasDrawImage/drawImage}}. Note: This should eventually be moved to the canvas section of the HTML specification. +See Issue 819. Drawing an image {#drawing-an-image} ==================================== -If a <> function image for a box is within the visual viewport, the user agent -must display an image output from an invocation of the draw a paint image algorithm. +If a <> function image for a [=box=] is within the visual viewport, the user agent +must display an image output from an invocation of the [=draw a paint image=] algorithm. -Note: The user agent doesn't have to run draw a paint image each frame for a <> +Note: The user agent doesn't have to run [=draw a paint image=] each frame for a <> function within the visual viewport. It can cache results, (potentially using additional invalidation steps) to display the correct image output. @@ -430,33 +429,33 @@ Note: The user agent can optionally defer drawing images which are outside the v });
And the element is inside the visual viewport, the user agent is required to - draw a paint image and display the result for the current frame. + [=draw a paint image=] and display the result for the current frame.
-The draw a paint image function is invoked by the user agent during the object size -negotiation algorithm which is responsible for rendering an <>, with -|snappedConcreteObjectSize| defined as follows. Let |concreteObjectSize| be the concrete object -size of the box. The |snappedConcreteObjectSize| is usually the same as the +The [=draw a paint image=] function is invoked by the user agent during the [=object size +negotiation=] algorithm which is responsible for rendering an <>, with +|snappedConcreteObjectSize| defined as follows. Let |concreteObjectSize| be the [=concrete object +size=] of the [=box=]. The |snappedConcreteObjectSize| is usually the same as the |concreteObjectSize|. However, the user agent may adjust the size such that it paints to pixel boundaries. If it does, the user agent should adjust the |snappedConcreteObjectSize| by the proportional change from its original size such that the <> function can adjust the drawing accordingly. -For the purposes of the object size negotiation algorithm, the paint image has no -intrinsic dimensions. +For the purposes of the [=object size negotiation=] algorithm, the paint image has no +[=intrinsic dimensions=]. -Note: In a future version of the spec, the author could have the ability to specify the intrinsic - dimensions of the paint image. This will probably be exposed as a callback allowing the - author to define static intrinsic dimensions or dynamically updating the intrinsic - dimensions based on computed style and size changes. +Note: In a future version of the spec, the author could have the ability to specify the [=intrinsic + dimensions=] of the paint image. This will probably be exposed as a callback allowing the + author to define static [=intrinsic dimensions=] or dynamically updating the [=intrinsic + dimensions=] based on computed style and size changes. The {{PaintSize}} object represents the size of the image that the author should draw. This is the |snappedConcreteObjectSize| given by the user agent. -Note: See [[css-images-3#object-sizing-examples]] for examples on how the concrete object - size is calculated. +Note: See [[css-images-3#object-sizing-examples]] for examples on how the [=concrete object + size=] is calculated. -The draw a paint image function may be speculatively invoked by the user agent at any point, +The [=draw a paint image=] function may be speculatively invoked by the user agent at any point, with any |snappedConcreteObjectSize|. The resulting image is not displayed. Note: User agents may use any heuristic to speculate a possible future value for @@ -482,26 +481,26 @@ with), given |snappedConcreteObjectSize| it must run the following step 2. Let |name| be the first argument of the |paintFunction|. - 3. Let |documentPaintDefinitionMap| be the associated document's document paint - definitions map. + 3. Let |documentPaintDefinitionMap| be the associated [=document's=] [=document paint + definitions=] map. - 4. If |documentPaintDefinitionMap|[|name|] does not exist, let the image output - be an invalid image and abort all these steps. + 4. If |documentPaintDefinitionMap|[|name|] does not [=map/exist=], let the image output + be an [=invalid image=] and abort all these steps. - 5. Let |documentDefinition| be the result of get + 5. Let |documentDefinition| be the result of [=map/get=] |documentPaintDefinitionMap|[|name|]. - 6. If |documentDefinition| is "invalid", let the image output be an invalid - image and abort all these steps. + 6. If |documentDefinition| is "invalid", let the image output be an [=invalid + image=] and abort all these steps. 7. Let |inputArgumentSyntaxes| be |documentDefinition|'s input argument syntaxes. - 8. Let |inputArguments| be the list of all the |paintFunction| arguments after + 8. Let |inputArguments| be the [=list=] of all the |paintFunction| arguments after the "paint name" argument. 9. If |inputArguments| do not match the registered grammar given by |inputArgumentSyntaxes|, let - the image output be an invalid image and abort all these steps. + the image output be an [=invalid image=] and abort all these steps.
This step may fail in the following cases: @@ -530,24 +529,24 @@ with), given |snappedConcreteObjectSize| it must run the following step </script>
- example-1 produces an invalid image as "red" does not + example-1 produces an [=invalid image=] as "red" does not match the registered grammar. - example-2 produces an invalid image as there are too many function + example-2 produces an [=invalid image=] as there are too many function arguments.
- 10. Let |workletGlobalScope| be a {{PaintWorkletGlobalScope}} from the list of worklet's - WorkletGlobalScopes from the paint {{Worklet}}, following the rules defined in + 10. Let |workletGlobalScope| be a {{PaintWorkletGlobalScope}} from the list of [=worklet's + WorkletGlobalScopes=] from the paint {{Worklet}}, following the rules defined in [[#global-scope-selection]]. - The user agent may also create a WorkletGlobalScope at this time, given the + The user agent may also [=create a WorkletGlobalScope=] at this time, given the paint {{Worklet}}. - 11. Run invoke a paint callback given |name|, |inputArguments|, |snappedConcreteObjectSize|, - |workletGlobalScope| optionally in parallel. + 11. Run [=invoke a paint callback=] given |name|, |inputArguments|, |snappedConcreteObjectSize|, + |workletGlobalScope| optionally [=in parallel=]. - Note: If the user agent runs invoke a paint callback on a thread in parallel, + Note: If the user agent runs [=invoke a paint callback=] on a thread [=in parallel=], it should select a paint worklet global scope which can be used on that thread.
@@ -555,57 +554,58 @@ with), given |snappedConcreteObjectSize| it must run the following step When the user agent wants to invoke a paint callback given |name|, |inputArguments|, |snappedConcreteObjectSize|, and |workletGlobalScope|, it must run the following steps: - 1. Let |paintDefinitionMap| be |workletGlobalScope|'s paint definitions map. + 1. Let |paintDefinitionMap| be |workletGlobalScope|'s [=paint definitions=] map. - 2. If |paintDefinitionMap|[|name|] does not exist, run the following steps: + 2. If |paintDefinitionMap|[|name|] does not [=map/exist=], run the following steps: - 1. Queue a task to run the following steps: + 1. [=Queue a task=] to run the following steps: - 1. Let |documentPaintDefinitionMap| be the associated document's document - paint definitions map. + 1. Let |documentPaintDefinitionMap| be the associated [=document=]'s [=document + paint definitions=] map. - 2. Set |documentPaintDefinitionMap|[|name|] to "invalid". + 2. [=map/Set=] |documentPaintDefinitionMap|[|name|] to "invalid". 3. The user agent should log an error to the debugging console stating that a class wasn't registered in all {{PaintWorkletGlobalScope}}s. - 2. Let the image output be an invalid image and abort all these steps. + 2. Let the image output be an [=invalid image=] and abort all these steps. Note: This handles the case where there could be a paint worklet global scope which didn't receive the {{registerPaint(name, paintCtor)}} for |name| (however another global scope did). A paint callback which is invoked on the other global scope could succeed, but - wont succeed on a subsequent frame when draw a paint image is called. + wont succeed on a subsequent frame when [=draw a paint image=] is called. - 3. Let |definition| be the result of get |paintDefinitionMap|[|name|]. + 3. Let |definition| be the result of [=get=] |paintDefinitionMap|[|name|]. - 4. Let |paintClassInstanceMap| be |workletGlobalScope|'s paint class instances map. + 4. Let |paintClassInstanceMap| be |workletGlobalScope|'s [=paint class instances=] map. - 5. Let |paintInstance| be the result of get |paintClassInstanceMap|[|name]|. If + 5. Let |paintInstance| be the result of [=get=] |paintClassInstanceMap|[|name]|. If |paintInstance| is null, run the following steps: - 1. If the constructor valid flag on |definition| is false, let the image output be an - invalid image and abort all these steps. + 1. If the [=paint definition/constructor valid flag=] on |definition| is false, let the image output be an + [=invalid image=] and abort all these steps. - 2. Let |paintCtor| be the class constructor on |definition|. + 2. Let |paintCtor| be the [=paint definition/class constructor=] on |definition|. - 3. Let |paintInstance| be the result of Construct(|paintCtor|). + 3. Let |paintInstance| be the result of [=Construct=](|paintCtor|). - If construct throws an exception, set the |definition|'s constructor valid - flag to false, let the image output be an invalid image and abort all these + If [=construct=] throws an exception, + set the |definition|'s [=paint definition/constructor valid flag=] to false, + let the image output be an [=invalid image=] and abort all these steps. - 4. Set |paintClassInstanceMap|[|name|] to |paintInstance|. + 4. [=map/Set=] |paintClassInstanceMap|[|name|] to |paintInstance|. - 6. Let |inputProperties| be |definition|'s input properties. + 6. Let |inputProperties| be |definition|'s [=paint definition/input properties=]. 7. Let |styleMap| be a new {{StylePropertyMapReadOnly}} populated with only the - computed value's for properties listed in |inputProperties|. + [=computed value=]'s for properties listed in |inputProperties|. - 8. Let |renderingContext| be the result of create a PaintRenderingContext2D object given: - - "width" - The width given by |concreteObjectSize|. - - "height" - The height given by |concreteObjectSize|. + 8. Let |renderingContext| be the result of [=create a PaintRenderingContext2D object=] given: + - "width" - The width given by |snappedConcreteObjectSize|. + - "height" - The height given by |snappedConcreteObjectSize|. - "paintRenderingContext2DSettings" - The - PaintRenderingContext2DSettings object given by |definition|. + [=paint definition/PaintRenderingContext2DSettings object=] given by |definition|. Note: The |renderingContext| is not be re-used between invocations of paint. Implicitly this means that there is no stored data, or state on the |renderingContext| between @@ -662,14 +662,14 @@ When the user agent wants to invoke a paint callback given |name|, |i
- 11. Let |paintFunctionCallback| be |definition|'s paint function. + 11. Let |paintFunctionCallback| be |definition|'s [=paint function=]. - 12. Invoke |paintFunctionCallback| with arguments «|renderingContext|, |paintSize|, - |styleMap|, |inputArguments|», and with |paintInstance| as the callback this value. + 12. [=Invoke=] |paintFunctionCallback| with arguments «|renderingContext|, |paintSize|, + |styleMap|, |inputArguments|», and with |paintInstance| as the [=callback this value=]. If |paintFunctionCallback| does not complete within an acceptable time (as determined by the user agent, i.e. it is a "long running script") the user agent may terminate the - script, let the image output be an invalid image, and abort all these steps. + script, let the image output be an [=invalid image=], and abort all these steps. Note: User agents could provide tooling within their debugging tools to show authors how expensive their paint classes are. User agents could also how an "unresponsive script" @@ -677,7 +677,7 @@ When the user agent wants to invoke a paint callback given |name|, |i 13. The image output is to be produced from the |renderingContext| given to the method. - If an exception is thrown the let the image output be an invalid image. + If an exception is [=thrown=] the let the image output be an [=invalid image=]. Note: The contents of the resulting image are not designed to be accessible. Authors can communicate any useful information through the standard accessibility APIs. @@ -686,8 +686,8 @@ Note: The contents of the resulting image are not designed to be accessible. Aut Global Scope Selection {#global-scope-selection} ------------------------------------------------ -When the user agent needs to select a {{PaintWorkletGlobalScope}} from the paint worklet's -WorkletGlobalScopes list it must: +When the user agent needs to select a {{PaintWorkletGlobalScope}} from the paint [=worklet's +WorkletGlobalScopes=] [=list=] it must: - Select from at least two {{PaintWorkletGlobalScope}}s, unless the user agent is under memory constraints. @@ -997,4 +997,26 @@ There are no known security issues introduced by these features. Privacy Considerations {#privacy-considerations} ================================================ -There are no known privacy issues introduced by these features. +* The timing of paint callbacks can be used as a high-bandwidth channel for detecting "visited" state for links. + (details) + This is not a fundamentally new privacy leak, + as visited state leaks from many interactions, + but absent any further mitigations, + this is a particularly high-bandwidth channel of the information. + + No official mitigations are planned at this time, + as this privacy leak needs to be addressed more directly + to fix all such channels. + +Changes {#changes} +================== + +Changes since the 9 August 2018 CR publication: + +* Filtered the list of input properties to a paint worklet to be only known or custom properties. + +* Added alpha flag to {{PaintRenderingContext2D}} to control whether the rendering surface is forced opaque or allows transparency. + +* Fix definition for the size of the output bitmap: + + > The size of the output bitmap is the concrete object size of the object it is rendering to size of the fragment it is rendering. diff --git a/css-paint-api/issues-cr-2018.bs b/css-paint-api/issues-cr-2018.bs new file mode 100644 index 00000000..65ee503f --- /dev/null +++ b/css-paint-api/issues-cr-2018.bs @@ -0,0 +1,57 @@ +Draft: https://www.w3.org/TR/2018/CR-css-paint-api-1-20180809/ +Title: CSS Painting API Level 1 +---- +Issue 1. +Summary: Broken LInks +From: Chris Lilley +Comment: https://github.com/w3c/css-houdini-drafts/issues/787 +Response: https://github.com/w3c/css-houdini-drafts/issues/787#issuecomment-411123499 +Closed: Accepted +Resolved: Editorial +---- +Issue 2. +Summary: CSS Paint API leaks browsing history +From: Deian Stefan +Comment: https://github.com/w3c/css-houdini-drafts/issues/791 +Response: https://github.com/w3c/css-houdini-drafts/issues/791#issuecomment-546460085 +Changes: https://github.com/w3c/css-houdini-drafts/commit/3c72275054d9d541e6526e2988567ad4d209f257 +Closed: Accepted +Resolved: Editor discretion +Verified: https://github.com/w3c/css-houdini-drafts/issues/791#issuecomment-546466424 +---- +Issue 3. +Summary: Typo: use snappedConcreteObjectSize in paint callback +From: Chris Harrelson +Comment: https://github.com/w3c/css-houdini-drafts/issues/800 +Changes: https://github.com/w3c/css-houdini-drafts/commit/735ea42edf09f8d69de1eddbc7e7691528f56963 +Closed: Accepted +Resolved: Editorial +---- +Issue 4. +Summary: Disconnect between css-paint-api-1 and HTML specs on CanvasImageSource +From: Alan Jeffrey +Comment: https://github.com/w3c/css-houdini-drafts/issues/819 +Response: https://github.com/w3c/css-houdini-drafts/issues/819#issuecomment-424360647 +Open: Waiting for HTML spec edits +---- +Issue 5. +Summary: Improve passing of large-scale data to PaintWorklet +From: John Wiesz +Comment: https://github.com/w3c/css-houdini-drafts/issues/872 +Response: https://github.com/w3c/css-houdini-drafts/issues/872#issuecomment-499903766 +Open: Waiting on edits to add new features to Properties an Values API +---- +Issue 6. +Summary: Cycle possibe using inputProperties() +From: Stephen McGruer +Comment: https://github.com/w3c/css-houdini-drafts/issues/877 +Response: https://github.com/w3c/css-houdini-drafts/issues/877#issuecomment-499922873 +Open: Needs Edits +---- +Issue 7. +Summary: Two-way communication between main thread and worklet +From: Samad Aghaei +Comment: https://github.com/w3c/css-houdini-drafts/issues/881 +Response: https://github.com/w3c/css-houdini-drafts/issues/881#issuecomment-546456591 +Closed: OutOfScope +---- diff --git a/css-paint-api/issues-cr-2018.html b/css-paint-api/issues-cr-2018.html new file mode 100644 index 00000000..9c7a0ccb --- /dev/null +++ b/css-paint-api/issues-cr-2018.html @@ -0,0 +1,128 @@ + + +CSS Painting API Level 1 Disposition of Comments for 2018-08-09 CR + + +

CSS Painting API Level 1 Disposition of Comments for 2018-08-09 CR

+ +

Dated Draft: https://www.w3.org/TR/2018/CR-css-paint-api-1-20180809/ + +

Editor's Draft: http://drafts.csswg.org/css-paint-api-1/ + +

The following color coding convention is used for comments:

+ + + +

Open issues are marked like this

+ +

An issue can be closed as Accepted, OutOfScope, +Invalid, Rejected, or Retracted. +Verified indicates commentor's acceptance of the response.

+
+Issue 1. #
+Summary:  Broken LInks
+From:     Chris Lilley
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/787
+Response: https://github.com/w3c/css-houdini-drafts/issues/787#issuecomment-411123499
+Closed:   Accepted
+Resolved: Editorial
+
+Issue 2. #
+Summary:  CSS Paint API leaks browsing history
+From:     Deian Stefan
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/791
+Response: https://github.com/w3c/css-houdini-drafts/issues/791#issuecomment-546460085
+Changes:  https://github.com/w3c/css-houdini-drafts/commit/3c72275054d9d541e6526e2988567ad4d209f257
+Closed:   Accepted
+Resolved: Editor discretion
+Verified: https://github.com/w3c/css-houdini-drafts/issues/791#issuecomment-546466424
+
+Issue 3. #
+Summary:  Typo: use snappedConcreteObjectSize in paint callback
+From:     Chris Harrelson
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/800
+Changes:  https://github.com/w3c/css-houdini-drafts/commit/735ea42edf09f8d69de1eddbc7e7691528f56963
+Closed:   Accepted
+Resolved: Editorial
+
+Issue 4. #
+Summary:  Disconnect between css-paint-api-1 and HTML specs on CanvasImageSource
+From:     Alan Jeffrey
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/819
+Response: https://github.com/w3c/css-houdini-drafts/issues/819#issuecomment-424360647
+Open:     Waiting for HTML spec edits
+
+Issue 5. #
+Summary:  Improve passing of large-scale data to PaintWorklet
+From:     John Wiesz
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/872
+Response: https://github.com/w3c/css-houdini-drafts/issues/872#issuecomment-499903766
+Open:     Waiting on edits to add new features to Properties an Values API
+
+Issue 6. #
+Summary:  Cycle possibe using inputProperties()
+From:     Stephen McGruer
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/877
+Response: https://github.com/w3c/css-houdini-drafts/issues/877#issuecomment-499922873
+Open:     Needs Edits
+
+Issue 7. #
+Summary:  Two-way communication between main thread and worklet
+From:     Samad Aghaei
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/881
+Response: https://github.com/w3c/css-houdini-drafts/issues/881#issuecomment-546456591
+Closed:   OutOfScope
+ diff --git a/css-paint-api/issues-list-2018-04-10.html b/css-paint-api/issues-list-2018-04-10.html new file mode 100644 index 00000000..34669236 --- /dev/null +++ b/css-paint-api/issues-list-2018-04-10.html @@ -0,0 +1,108 @@ + + +CSS Paint API Level 1 Disposition of Comments for 2018-04-10 WD + + +

CSS Paint API Level 1 Disposition of Comments for 2018-04-10 WD

+ +

Dated Draft: https://www.w3.org/TR/2018/WD-css-paint-api-1-20180410/ + +

Editor's Draft: http://drafts.csswg.org/css-paint-api-1/ + +

The following color coding convention is used for comments:

+ + + +

Open issues are marked like this

+ +

An issue can be closed as Accepted, OutOfScope, +Invalid, Rejected, or Retracted. +Verified indicates commentor's acceptance of the response.

+
+Issue 1. #
+Summary:  Filter out unsupported properties from inputProperties.
+From:     Darren Shen
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/523
+Response: https://github.com/w3c/css-houdini-drafts/issues/523#issuecomment-351219067
+Changes:  https://github.com/w3c/css-houdini-drafts/commit/50fa9b8bedde46462113d756d6895701d3d743d0
+Closed:   Accepted
+Resolved: Editor discretion
+
+Issue 2. #
+Summary:  Allow inputArguments define optional arguments.
+From:     zheeeng
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/763
+Response: https://github.com/w3c/css-houdini-drafts/issues/763#issuecomment-401653404
+Closed:   Deferred
+
+Issue 3. #
+Summary:  Need to convert paint function to WebIDL Function type.
+From:     Shiino Yuki
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/743
+Response: https://github.com/w3c/css-houdini-drafts/issues/743#issuecomment-379738324
+Changes:  https://github.com/w3c/css-houdini-drafts/commit/f9f174dca09f5149c3df558d1c8e74517df19f5e
+Closed:   Accepted
+Resolved: https://lists.w3.org/Archives/Public/public-houdini/2018Apr/0002.html 
+
+Issue 4. #
+Summary:  Use WebIDL callback interface inside of registerPaint.
+From:     Shiino Yuki
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/743
+Response: https://github.com/w3c/css-houdini-drafts/issues/743#issuecomment-379738324
+Closed:   Rejected
+Resolved: https://lists.w3.org/Archives/Public/public-houdini/2018Apr/0002.html 
+Verified: https://github.com/w3c/css-houdini-drafts/issues/743#issuecomment-379646250
+ diff --git a/css-paint-api/issues-list-2018-04-10.txt b/css-paint-api/issues-list-2018-04-10.txt new file mode 100644 index 00000000..41cef9ac --- /dev/null +++ b/css-paint-api/issues-list-2018-04-10.txt @@ -0,0 +1,38 @@ +Draft: https://www.w3.org/TR/2018/WD-css-paint-api-1-20180410/ +Title: CSS Paint API Level 1 + +---- +Issue 1. +Summary: Filter out unsupported properties from inputProperties. +From: Darren Shen +Comment: https://github.com/w3c/css-houdini-drafts/issues/523 +Response: https://github.com/w3c/css-houdini-drafts/issues/523#issuecomment-351219067 +Changes: https://github.com/w3c/css-houdini-drafts/commit/50fa9b8bedde46462113d756d6895701d3d743d0 +Closed: Accepted +Resolved: Editor discretion +---- +Issue 2. +Summary: Allow inputArguments define optional arguments. +From: zheeeng +Comment: https://github.com/w3c/css-houdini-drafts/issues/763 +Response: https://github.com/w3c/css-houdini-drafts/issues/763#issuecomment-401653404 +Closed: Deferred +---- +Issue 3. +Summary: Need to convert paint function to WebIDL Function type. +From: Shiino Yuki +Comment: https://github.com/w3c/css-houdini-drafts/issues/743 +Response: https://github.com/w3c/css-houdini-drafts/issues/743#issuecomment-379738324 +Changes: https://github.com/w3c/css-houdini-drafts/commit/f9f174dca09f5149c3df558d1c8e74517df19f5e +Closed: Accepted +Resolved: https://lists.w3.org/Archives/Public/public-houdini/2018Apr/0002.html +---- +Issue 4. +Summary: Use WebIDL callback interface inside of registerPaint. +From: Shiino Yuki +Comment: https://github.com/w3c/css-houdini-drafts/issues/743 +Response: https://github.com/w3c/css-houdini-drafts/issues/743#issuecomment-379738324 +Closed: Rejected +Resolved: https://lists.w3.org/Archives/Public/public-houdini/2018Apr/0002.html +Verified: https://github.com/w3c/css-houdini-drafts/issues/743#issuecomment-379646250 +---- diff --git a/css-properties-values-api/Overview.bs b/css-properties-values-api/Overview.bs index faa6e95d..1caace06 100644 --- a/css-properties-values-api/Overview.bs +++ b/css-properties-values-api/Overview.bs @@ -23,24 +23,29 @@ Ignored Terms: construct, name map of inputs Ignored Vars: arguments, methodPropertyKey, inputStyleMap, workletGlobalScope Ignored Terms: WorkletGlobalContext Repository: w3c/css-houdini-drafts - - -
-{
-	"css-paint-api": {
-		"title": "CSS Painting API"
-	},
-	"css-layout-api": {
-		"title": "CSS Layout API"
-	}
-}
+Markup Shorthands: css on, markdown on
 
Introduction {#intro} @@ -56,58 +61,563 @@ can only impact document layout or paint by being re-incorporated into the value of other properties via a var() reference. This specification extends [[css-variables]], allowing the registration of properties -that have a value type, an initial value, and a defined inheritance behaviour. +that have a value type, an initial value, and a defined inheritance behaviour, +via two methods: + +* A JS API, the {{registerProperty()}} method +* A CSS at-rule, the ''@property'' rule -This specification is complementary to [[css-paint-api]] and [[css-layout-api]], which +This specification is complementary to [[css-paint-api-1]] and [[css-layout-api-1]], which allow custom properties to directly impact paint and layout behaviours respectively. -Registering custom properties {#registering-custom-properties} -============================================================== + + + + +Registered Custom Properties {#behavior-of-custom-properties} +============================================================= + +A [=custom property=] can become a registered custom property, +making it act more like a UA-defined property: +giving it a syntax that's checked by the UA, +an initial value, +and a specific inheritance behavior. +This can be done by the ''@property'' rule, +or the {{registerProperty()}} JS function. + +A [=custom property=] is considered to be registered for a {{Document}} +if there is a valid ''@property'' rule +defined for its name +in one of the document's stylesheets, +or its name is [=map/contains|contained=] +in the document's {{[[registeredPropertySet]]}} slot +(that is, {{registerProperty()}} was called to register it). + +A [=registered custom property=] acts similarly to an unregistered [=custom property=], +except as defined below. + +Determining the Registration {#determining-registration} +-------------------------------------------------------- + +A [=registered custom property=] has a custom property registration +that contains all the data necessary to treat it like a real property. +It's a [=struct=] consisting of: + +* a property name (a [=custom property name string=]) +* a syntax (a [=syntax string=]) +* an inherit flag (a [=boolean=]) +* optionally, an initial value (a [=string=] which successfully [=CSS/parses=] according to the syntax) + +If the {{Document}}’s {{[[registeredPropertySet]]}} slot +[=set/contains=] a record with the [=custom property’s=] name, +the registration is that record. + +Otherwise, +if the {{Document}}’s active stylesheets contain at least one valid ''@property'' rule +representing a registration with the [=custom property’s=] name, +the last such one in document order is the registration. + +Otherwise there is no registration, +and the [=custom property=] is not a [=registered custom property=]. + +Parse-Time Behavior {#parsing-custom-properties} +------------------------------------------------ + +[=Registered custom properties=] parse exactly like unregistered [=custom properties=]; +almost anything is allowed. +The registered syntax of the property is not checked at parse time. + +Note: However, +the syntax is checked at computed-value time, +before substitution via ''var()''. +See [[#calculation-of-computed-values]]. + +
+ Why aren't custom properties syntax-checked? + + When parsing a page's CSS, + UAs commonly make a number of optimizations + to help with both speed and memory. + + One of those optimizations + is that they only store the properties that will actually have an effect; + they throw away invalid properties, + and if you write the same property multiple times in a single declaration block, + all but the last valid one will be thrown away. + (This is an important part of CSS's error-recovery + and forward-compatibility behavior.) + + This works fine if the syntax of a property never changes over the lifetime of a page. + If a custom property is registered, however, + it can change its syntax, + so that a property that was previously invalid + suddenly becomes valid. + + The only ways to handle this are to either store every declaration, + even those that were initially invalid + (increasing the memory cost of pages), + or to re-parse the entire page's CSS + with the new syntax rules + (increasing the processing cost of registering a custom property). + Neither of these are very desirable. + + Further, + UA-defined properties have their syntax determined + by the version of the UA the user is viewing the page with; + this is out of the page author's control, + which is the entire reason for CSS's error-recovery behavior + and the practice of writing multiple declarations for varying levels of support. + A custom property, on the other hand, + has its syntax controlled by the page author, + according to whatever stylesheet or script they've included in the page; + there's no unpredictability to be managed. + Throwing away syntax-violating custom properties + would thus only be, at best, a convenience for the page author, + not a necessity like for UA-defined properties. +
+ +[=Specified Value=]-Time Behavior {#specified-value} +---------------------------------------------------- + +Just like unregistered [=custom properties=], +all [=registered custom properties=], regardless of registered syntax, +accept the [=CSS-wide keywords=], +such as ''inherit'' or ''revert''. +Their behavior is defined in [[css-cascade-4#defaulting-keywords]]. + +[=Computed Value=]-Time Behavior {#calculation-of-computed-values} +------------------------------------------------------------------ + +The [=computed value=] of a [=registered custom property=] +is determined by the syntax of its [=registration=]. + +If the [=registration’s=] syntax is the [=universal syntax definition=], +the [=computed value=] is the same as for unregistered [=custom properties=] +(either the specified value with variables substituted, +or the [=guaranteed-invalid value=]). + +Otherwise, attempt to [=CSS/parse=] the property's value +according to its registered syntax. +If this fails, +the [=computed value=] is the [=guaranteed-invalid value=]. +If it succeeds, +the [=computed value=] depends on the specifics of the syntax: + +For "<length>", +"<length-percentage>", +"<angle>", +"<time>", +"<resolution>", +"<integer>", +"<number>", +and "<percentage>" values: + +* If the specified value is a [=dimension=] literal + (such as ''50em'' or ''.2s''), + the computed value is the same value, + but with the unit converted to the corresponding [=canonical unit=] + for the type of value. +* If the specified value is any other numeric literal + (such as ''5'' or ''20%''), + the computed value is as specified. + (In particular, percentages are never resolved against anything.) +* If the specified value is a function that evaluates to one of those types + (such as a [=math function=]), + the computed value is defined by that function. + +For "<color>" values, +the value is computed by [=resolving color values=]. + +For "<custom-ident>", ident, or "*" values, +the computed value is as specified. + +For "<url>" values, +the computed value is one of the following: + +* if the URL is a relative URL, + the computed value is the resolved absolute URL as described in [[!css3-values]]. +* otherwise, the computed value is as specified. + +
+ URL behavior examples +
+ Because URLs resolve against the base URL of the stylesheet they appear in, we can + end up with multiple relative URLs that resolve against different base URLs, even though + they appear in the same property. + + For example, suppose '--url-foo' and '--url-bar' are registered + custom properties with ''<url>'' syntax, and that we have a stylesheet at + /style/foo/foo.css: + +
+		div {
+			--url-foo: url("foo.png");
+		}
+		
+ + and another stylesheet at /style/bar/bar.css +
+		div {
+			--url-bar: url("bar.png");
+		}
+		
+ + and finally a document at /index.html: +
+		<link href="/style/foo/foo.css" rel="stylesheet" type="text/css">
+		<link href="/style/bar/bar.css" rel="stylesheet" type="text/css">
+		<div style="background-image: var(--url-foo), var(---url-bar);">
+		</div>
+		
+ + Here, the ''var(--url-foo)'' reference would produce a URL that resolves against + /style/foo, and the ''var(--url-bar)'' reference would produce a URL that resolves + against /style/bar. + + On the other hand, + if both '--url-foo' and '--url-bar' were unregistered, + they would substitute their literal values + (relative URLs) + into the /index.html stylesheet, + which would then resolve the URLs against /index.html instead. +
+
+ +For "<image>" values, +the computed value is the [=computed <image>=]. + +For "<transform-function>" and "<transform-list>" values, +the computed value is as specified but with all lengths resolved to their computed values. + +For values with [[#multipliers|multipliers]], +the computed value is a list of the computed values of the base type. + +For syntaxes specified with [[#combinator|the | combinator]], +the computed value is given by applying the computed-value rules +for the first clause that matches the value. + + +Animation Behavior {#animation-behavior-of-custom-properties} +------------------------------------------------------------- + +Note: As defined by [[css3-animations]] and [[css3-transitions]], it is possible to +specify animations and transitions that reference custom properties. + +When referenced by animations and transitions, +custom property values [=interpolate=] [=by computed value=], +in accordance with the type that they parsed as. + +Note: This implies that a list of values, +such as `+` or `#`, +will interpolate as a simple list, +matching up each component index-by-index, +and failing if the number of components doesn't match. + +As an exception to the above rule, +a value that parsed as a ``, +a ``, +or a `+` +instead interpolates as per the 'transform' property. + +Note: If, +for whatever reason, +a custom property is defined with a syntax of `#`, +this will thus first interpolate as a simple list, +and then each list item will interpolate as a 'transform' value. + +Note: Registering (or changing the registration) of a custom property +can change its computed value, +which can start or interrupt a CSS transition. + +Conditional Rules {#conditional-rules} +-------------------------------------- + +As stated in [[#parsing-custom-properties]], +both unregistered and [=registered=] [=custom properties=] +accept (almost) all possible values at parse-time. +[=Registered=] [=custom properties=] only apply their syntax at [=computed value=] time. + +So, all [=custom properties=], +regardless of whether they're [=registered=] or unregistered, +will test as "true" in an ''@supports'' rule, +so long as you don't violate the (very liberal) generic syntax for [=custom properties=]. + +
+ For example, + even if a custom property is registered + with syntax: "<color>";, + a rule like `@supports (--foo: 1em) {...}` + will still evaluate as true and apply those styles, + because the declaration does successfully parse as a valid property. +
+ + +Substitution via ''var()'' {#substitution} +------------------------------------------ + +Like unregistered custom properties, +the value of a registered custom property can be substituted into another value with the ''var()'' function. +However, registered custom properties substitute as their [[#calculation-of-computed-values|computed value]], +rather than the original token sequence used to produce that value. + +Any ''var()'' function that references a registered custom property +must be replaced with an equivalent token sequence, +which is equal to the token sequence that would have been produced +by [=serialize a CSS value|serializing=] the computed value, +and [[css-syntax-3#tokenization|tokenizing]] the resulting string. + +
+ Suppose that '--x' is registered with ''<length>'' syntax, + and that '--y'is an unregistered custom property. + +
+
+	div {
+		font-size: 10px;
+		--x: 8em;
+		--y: var(--x);
+	}
+	
+ + Because the computed value of '--x' (when serialized) is "80px", + the computed value of '--y' is + a <> with a value of "80" and unit "px". +
+ +### Fallbacks In ''var()'' References ### {#fallbacks-in-var-references} + +References to registered custom properties using the ''var()'' function may +provide a fallback. However, the fallback value must match the +syntax definition of the custom property being referenced, otherwise the +declaration is invalid at computed-value time. + +Note: This applies regardless of whether or not the fallback is being used. + + +### Dependency Cycles via Relative Units ### {#dependency-cycles} + +[=Registered custom properties=] follow the same rules for dependency cycle resolution +as unregistered [=custom properties=], +with the following additional constraints: + +For any registered custom property +with a <> or <> syntax component: + +* If the property contains any of the following units: + ''em'', ''ex'', ''cap'', ''ch'', ''ic'', ''lh''; + then add an edge between the property + and the ''font-size'' of the current element. +* If the property contains the ''lh'' unit, + add an edge between the property + and the ''line-height'' of the current element. +* If the property contains any of the following units: ''rem'', ''rlh''; + then add an edge between the property + and the 'font-size'' of the root element. +* If the property contains the ''rlh'' unit, + add an edge between the property + and the 'line-height'' of the root element. + +
+ For example, given this registration: + +
+	CSS.registerProperty({
+	  name: "--my-font-size",
+	  syntax: "<length>",
+	  initialValue: "0px",
+	  inherits: false
+	});
+	
+ + the following will produce a dependency cycle: + +
+	div {
+		--my-font-size: 10em;
+		font-size: var(--my-font-size);
+	}
+	
+ + and ''font-size'' will behave as if the value ''unset'' was specified. +
+ + + + + +The @property Rule {#at-property-rule} +================================================= + +The ''@property'' rule represents a [=custom property registration=] +directly in a stylesheet +without having to run any JS. +Valid ''@property'' rules result in a [=registered custom property=], +as if {{registerProperty()}} had been called with equivalent parameters. + +The syntax of ''@property'' is: + +
+	@property <> {
+		<>
+	}
+	
+ +A valid ''@property'' rule represents a [=custom property registration=], +with the property name being the serialization of the <> +in the rule's prelude. + +''@property'' rules require a 'syntax' and 'inherits' descriptor; +if either are missing, +the entire rule is invalid and must be ignored. +The 'initial-value' descriptor is optional +only if the syntax is the [=universal syntax definition=], +otherwise the descriptor is required; +if it's missing, the entire rule is invalid and must be ignored. + +Unknown descriptors are invalid and ignored, +but do not invalidate the ''@property'' rule. + +Note: As specified in [[#determining-registration]], +if multiple valid ''@property'' rules are defined for the same <>, +the last one in stylesheet order "wins". +A custom property registration from {{registerProperty()|CSS.registerProperty()}} +further wins over any ''@property'' rules +for the same <>. + +A ''@property'' is invalid if it occurs in a stylesheet inside of a [=shadow tree=], +and must be ignored. + +Issue(939): This will likely change in the future, +as the behavior of concept-defining at-rules in shadow trees +becomes more consistently defined. + +The 'syntax' Descriptor {#the-syntax-descriptor} +------------------------------------------------ + +
+	Name: syntax
+	Value: <>
+	For: @property
+	Initial: n/a (see prose)
+	
+ +Specifies the syntax of the [=custom property registration=] +represented by the ''@property'' rule, +controlling how the property's value is parsed at [=computed value=] time. + +The 'syntax' descriptor is required for the ''@property'' rule to be valid; +if it's missing, the ''@property'' rule is invalid. + +If the provided string is not a valid [=syntax string=] +(if it returns failure when [=consume a syntax definition=] is called on it), +the descriptor is invalid and must be ignored. + + +The 'inherits' Descriptor {#inherits-descriptor} +------------------------------------------------ + +
+	Name: inherits
+	Value: true | false
+	For: @property
+	Initial: n/a (see prose)
+	
+ +Specifies the inherit flag of the [=custom property registration=] +represented by the ''@property'' rule, +controlling whether or not the property inherits by default. + +The 'inherits' descriptor is required for the ''@property'' rule to be valid; +if it's missing, the ''@property'' rule is invalid. + + +The 'initial-value' Descriptor {#initial-value-descriptor} +---------------------------------------------------------- + +
+	Name: initial-value
+	Value: <>
+	For: @property
+	Initial: the [=guaranteed-invalid value=] (but see prose)
+	
+ +Specifies the initial value of the [=custom property registration=] +represented by the ''@property'' rule, +controlling the property’s [=initial value=]. + +If the value of the 'syntax' descriptor is the [=universal syntax definition=], +then the 'initial-value' descriptor is optional. +If omitted, the [=initial value=] of the property is the [=guaranteed-invalid value=]. + +Otherwise, +if the value of the 'syntax' descriptor is not the [=universal syntax definition=], +the following conditions must be met for the the ''@property'' rule to be valid: + + * The 'initial-value' descriptor must be present. + * The 'initial-value' descriptor's value must [=consume a syntax definition|parse successfully=] + according to the grammar specified by the [=syntax definition=]. + * The 'initial-value' must be [=computationally independent=]. + +If the above conditions are not met, the ''@property'' rule is invalid. + + + + +Registering Custom Properties in JS {#registering-custom-properties} +==================================================================== + +To register a custom property via JS, +the {{CSS}} object is extended with a {{registerProperty()}} method:
-dictionary PropertyDescriptor {
+dictionary PropertyDefinition {
 	required DOMString name;
 	         DOMString syntax       = "*";
-	         boolean   inherits     = false;
+	required boolean   inherits;
 	         DOMString initialValue;
 };
 
-partial interface CSS {
-	static void registerProperty(PropertyDescriptor descriptor);
+partial namespace CSS {
+	undefined registerProperty(PropertyDefinition definition);
 };
 
Additional, the {{Document}} object gains a new \[[registeredPropertySet]] private slot, which is a set of records that describe registered custom properties. -The {{PropertyDescriptor}} dictionary {#the-propertydescriptor-dictionary} --------------------------------------------------------------------------- +The {{registerProperty()}} Function {#the-registerproperty-function} +-------------------------------------------------------------------- -A PropertyDescriptor dictionary represents author-specified configuration -options for a custom property. {{PropertyDescriptor}} dictionaries contain the -following members: - -: name -:: The name of the custom property being defined. - -: syntax -:: A string representing how this custom property is parsed. - -: inherits -:: True if this custom property should inherit down the DOM tree; False otherwise. - -: initialValue -:: The initial value of this custom property. - -The {{registerProperty()}} function {#the-registerproperty-function} --------------------------------------------------------------------------------------------------- - -The registerProperty(PropertyDescriptor descriptor) method +The registerProperty(PropertyDefinition definition) method registers a custom property according to the configuration options provided in -descriptor. +definition. When it is called, it executes the register a custom property algorithm, -passing the options in its descriptor argument +passing the options in its definition argument as arguments of the same names.
@@ -125,40 +635,21 @@ as arguments of the same names. associated Document's {{[[registeredPropertySet]]}} slot. - 2. Attempt to parse |name| - as a <>. - If this fails, + 2. If |name| is not a [=custom property name string=], throw a {{SyntaxError}} and exit this algorithm. - Otherwise, - let |parsed name| be the parsed value. - If |property set| - already contains an entry with |parsed name| as its property name + already contains an entry with |name| as its property name (compared codepoint-wise), throw an {{InvalidModificationError}} and exit this algorithm. - 3. If |syntax| is not present, - or is equal to "*" (U+002A ASTERISK), - let |parsed syntax| be undefined, - and skip to the next step of this algorithm. + 3. Attempt to [=consume a syntax definition=] from |syntax|. + If it returns failure, throw a {{SyntaxError}}. + Otherwise, let |syntax definition| be the returned syntax definition. - Otherwise, attempt to parse |syntax| - according to the rules in [[#supported-syntax-strings]]. - If it does not parse successfully, - throw a {{SyntaxError}}. - Otherwise, - let |parsed syntax| be the parsed syntax. - - Note: For example, a valid syntax string is something like "<length>", - or "<number>+"; - the allowed syntax is a subset of [[css-values-3#value-defs]]. - Future levels of this specification are expected to expand the complexity of allowed syntax strings, - allowing custom properties that more closely resemble the full breadth of what CSS properties allow. - - 4. If |parsed syntax| is undefined, + 4. If |syntax definition| is the universal syntax definition, and |initialValue| is not present, let |parsed initial value| be empty. This must be treated identically to the "default" initial value of custom properties, @@ -166,8 +657,8 @@ as arguments of the same names. Skip to the next step of this algorithm. Otherwise, - if |parsed syntax| is undefined, - parse |initialValue| as a <>. + if |syntax definition| is the universal syntax definition, + [=CSS/parse=] |initialValue| as a <>. If this fails, throw a {{SyntaxError}} and exit this algorithm. @@ -180,8 +671,8 @@ as arguments of the same names. and exit this algorithm. Otherwise, - parse {{PropertyDescriptor/initialValue}} - according to |parsed syntax|. + [=CSS/parse=] {{PropertyDefinition/initialValue}} + according to |syntax definition|. If this fails, throw a {{SyntaxError}} and exit this algorithm. @@ -191,16 +682,14 @@ as arguments of the same names. throw a {{SyntaxError}} and exit this algorithm. - 5. If |inherits| is present, - set |inherit flag| to its value. - Otherwise, set |inherit flag| to false. + 5. Set |inherit flag| to the value of |inherits|. - 6. Let |registered property| be a record - with a property name of |parsed name|, - a syntax of |parsed syntax|, + 6. Let |registered property| be a [=struct=] + with a property name of |name|, + a syntax of |syntax definition|, an initial value of |parsed initial value|, and an inherit flag of |inherit flag|. - Add |registered property| + [=set/Append=] |registered property| to |property set|.
@@ -231,9 +720,15 @@ as described in [[#calculation-of-computed-values]]. Note: A way to unregister properties may be added in the future. -When the current global object's associated Document's {{[[registeredPropertySet]]}} changes, -previously syntactically invalid property values can become valid and vice versa. -This can change the set of declared values which requires the cascade to be recomputed. +Registering a custom property must not affect the [=cascade=] in any way. +Regardless of what syntax is specified for a registered property, +at parse time it is still parsed as normal for a [=custom property=], +accepting nearly anything. +If the [=specified value=] for a [=registered custom property=] +violates the registered syntax, +however, +the property becomes [=invalid at computed-value time=] +(and thus resets to the registered initial value).
By default, all custom property declarations that can be parsed as a sequence of tokens @@ -251,9 +746,11 @@ This can change the set of declared values which requires the cascade< The second '--my-color' declaration overrides the first at parse time (both are valid), and the ''var()'' reference in the 'color' property is found to be invalid at computed-value time (because ''url("not-a-color")'' is not a color). - At this stage of the CSS pipeline (computation time), the only available fallback is the initial value - of the property, which in the case of color is ''inherit''. Although there was a valid usable value - (green), this was removed during parsing because it was superseded by the URL. + At this stage of the CSS pipeline (computation time), + the only available fallback is the initial value of the property, + which in the case of color is ''inherit''. + Although there was a valid usable value (green), + this was removed during parsing because it was superseded by the URL. If we call: @@ -261,20 +758,104 @@ This can change the set of declared values which requires the cascade< CSS.registerProperty({ name: "--my-color", syntax: "<color>", - initialValue: "black" + initialValue: "black", + inherits: false }); - then the second '--my-color' declaration becomes syntactically invalid at parse time, - and is ignored. - The first '--my-color' is the only valid declaration left for the property, - so 'color' is set to the value ''green''. + the parsing doesn't significantly change, + regardless of whether the registration occurs before or after the stylesheet above. + The only difference is that it's the '--my-color' property that becomes [=invalid at computed-value time=] instead + and gets set to its initial value of ''black''; + then 'color' is validly set to ''black'', + rather than being [=invalid at computed-value time=] + and becoming ''inherit''.
-Supported syntax strings {#supported-syntax-strings} ----------------------------------------------------- +The {{PropertyDefinition}} Dictionary {#the-propertydefinition-dictionary} +-------------------------------------------------------------------------- + +A PropertyDefinition dictionary represents author-specified configuration +options for a custom property. {{PropertyDefinition}} dictionaries contain the +following members: + +: name +:: The name of the custom property being defined. + +: syntax +:: A string representing how this custom property is parsed. + +: inherits +:: True if this custom property should inherit down the DOM tree; False otherwise. + +: initialValue +:: The initial value of this custom property. + + + + +Syntax Strings {#syntax-strings} +================================ + +A syntax string describes the value types accepted by a registered +custom property. Syntax strings consists of +[=syntax component names=], that are +optionally [[#multipliers|multiplied]] and [[#combinator|combined]]. -The following syntax strings are supported: +A syntax string can be parsed into a syntax definition, which is either: + + 1. A list of syntax components, each of which accept the value types + specified in [[#supported-names]], or + 2. The universal syntax definition ('*'), which accepts any valid token + stream. + +Note: Regardless of the syntax specified, all custom properties accept +CSS-wide keywords, and process these values +appropriately. + +
+ For example, the following are all valid syntax strings. + + : "<length>" + :: accepts length values + : "<length> | <percentage>" + :: accepts lengths, percentages, percentage calc expressions, and length calc + expressions, but not calc expressions containing a combination of length + and percentage values. + : "<length-percentage>" + :: accepts all values that "<length> | <percentage>" would + accept, as well as calc expressions containing a combination of both length + and percentage values. + : "big | bigger | BIGGER" + :: accepts the ident big, or the ident bigger, or + the ident BIGGER. + : "<length>+" + :: accepts a space-separated list of length values. + : "*" + :: accepts any valid token stream +
+ +Note: The internal grammar of syntax strings is a subset of +[[css-values-3#value-defs|the CSS Value Definition Syntax]]. Future levels of this specification are expected +to expand the complexity of the allowed grammar, allowing custom properties +that more closely resemble the full breadth of what CSS properties allow. + +The remainder of this chapter describes the internal grammar of the syntax +strings. + +Supported Names {#supported-names} +---------------------------------- + +This section defines the supported syntax component names, and the +corresponding types accepted by the resulting syntax component. : "<length>" :: Any valid <> value @@ -299,144 +880,344 @@ The following syntax strings are supported: :: Any valid <