Skip to content

Commit 064af7e

Browse files
authored
UIComponent: Deprecate animationFrame
See the kdocs on `ElementaVersion.V8` for details and motivation. Aside from the changes behind the ElementaVersion flag, this commit also changes the behavior of constraints evaluated on components which do not have a window (because we therefore cannot determine the ElementaVersion) to never cache its value. This is necessary because if we were to cache its value in that situation, we wouldn't be able to get it registered in the Window's `cachedConstraints` list, and so it'd never be invalidated again on V8+. The chance of someone relying on the constraint caching behavior in such a specific scenario are sufficiently tiny that it seems safe to make this change. GitHub: #157
1 parent c199a84 commit 064af7e

23 files changed

+474
-145
lines changed

api/Elementa.api

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public final class gg/essential/elementa/ElementaVersion : java/lang/Enum {
88
public static final field V5 Lgg/essential/elementa/ElementaVersion;
99
public static final field V6 Lgg/essential/elementa/ElementaVersion;
1010
public static final field V7 Lgg/essential/elementa/ElementaVersion;
11+
public static final field V8 Lgg/essential/elementa/ElementaVersion;
1112
public final fun enableFor (Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
1213
public static fun valueOf (Ljava/lang/String;)Lgg/essential/elementa/ElementaVersion;
1314
public static fun values ()[Lgg/essential/elementa/ElementaVersion;
@@ -362,7 +363,6 @@ public final class gg/essential/elementa/components/ScrollComponent : gg/essenti
362363
public synthetic fun addChild (Lgg/essential/elementa/UIComponent;)Lgg/essential/elementa/UIComponent;
363364
public fun addChild (Lgg/essential/elementa/UIComponent;)Lgg/essential/elementa/components/ScrollComponent;
364365
public final fun addScrollAdjustEvent (ZLkotlin/jvm/functions/Function2;)V
365-
public fun afterInitialization ()V
366366
public fun alwaysDrawChildren ()Z
367367
public fun animationFrame ()V
368368
public fun childrenOfType (Ljava/lang/Class;)Ljava/util/List;
@@ -756,6 +756,8 @@ public final class gg/essential/elementa/components/Window : gg/essential/elemen
756756
public final fun drawFloatingComponents (Lgg/essential/universal/UMatrixStack;)V
757757
public final fun focus (Lgg/essential/elementa/UIComponent;)V
758758
public final fun getAnimationFPS ()I
759+
public final fun getAnimationTimeMs ()J
760+
public final fun getAnimationTimeNs ()J
759761
public fun getBottom ()F
760762
public final fun getFocusedComponent ()Lgg/essential/elementa/UIComponent;
761763
public final fun getHasErrored ()Z
@@ -766,6 +768,7 @@ public final class gg/essential/elementa/components/Window : gg/essential/elemen
766768
public fun getTop ()F
767769
public fun getWidth ()F
768770
public fun hitTest (FF)Lgg/essential/elementa/UIComponent;
771+
public final fun invalidateCachedConstraints ()V
769772
public final fun isAreaVisible (DDDD)Z
770773
public fun keyType (CI)V
771774
public fun mouseClick (DDI)V
@@ -1871,7 +1874,6 @@ public final class gg/essential/elementa/constraints/RainbowColorConstraint : gg
18711874
public fun <init> ()V
18721875
public fun <init> (IF)V
18731876
public synthetic fun <init> (IFILkotlin/jvm/internal/DefaultConstructorMarker;)V
1874-
public fun animationFrame ()V
18751877
public final fun getAlpha ()I
18761878
public fun getCachedValue ()Ljava/awt/Color;
18771879
public synthetic fun getCachedValue ()Ljava/lang/Object;

src/main/kotlin/gg/essential/elementa/ElementaVersion.kt

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
package gg.essential.elementa
22

3+
import gg.essential.elementa.components.UpdateFunc
4+
import gg.essential.elementa.components.Window
5+
import gg.essential.elementa.constraints.SuperConstraint
6+
import gg.essential.elementa.constraints.animation.AnimationComponent
7+
import gg.essential.elementa.effects.Effect
8+
39
/**
410
* Sometimes it is necessary or desirable to introduce breaking behavioral changes to Elementa. In order to maintain
511
* full backwards compatibility in these cases, library consumers must explicitly opt-in to such changes for their
@@ -96,8 +102,76 @@ enum class ElementaVersion {
96102
/**
97103
* [gg.essential.elementa.components.Window] now disables input events if an error has occurred during drawing.
98104
*/
105+
@Deprecated(DEPRECATION_MESSAGE)
99106
V7,
100107

108+
/**
109+
* The [animationFrame][UIComponent.animationFrame] methods are now deprecated and will no longer be called at all
110+
* for [constraints][SuperConstraint.animationFrame] or if your override is marked as [Deprecated].
111+
* The relative order in which various things ([UpdateFunc]s, constraint cache invalidation, [UIComponent] timers
112+
* and field animations, [UIComponent.animationFrame], and [Effect.animationFrame]) will be called has changed
113+
* because constraint cache invalidation is separate now and [UpdateFunc]s are used internally for timers and field
114+
* animations now.
115+
*
116+
* All custom constraints which currently rely on `animationFrame` must be updated to support the new
117+
* `animationTime` mechanism described below before this version can be enabled!
118+
*
119+
* All custom components and effects which override `animationFrame` should be updated to use the [UpdateFunc] API
120+
* instead, and some may also require updates to account for the change in relative order mentioned above.
121+
* Note however that both the UpdateFunc mechanism and the animationTime properties are both available on any
122+
* [ElementaVersion], so most (if not all) of your components can migrate to them even before opting to enable
123+
* this [ElementaVersion].
124+
*
125+
* If your custom component or effect needs to update some animation or other miscellaneous state before each frame,
126+
* use the [UpdateFunc] mechanism instead (via [UIComponent.addUpdateFunc]/[Effect.addUpdateFunc]).
127+
* This way, only components which actually have something that needs updating will need to be called each frame.
128+
*
129+
* If your custom component or effect needs to continue to support older [ElementaVersion]s, ideally mark your
130+
* `animationFrame` override as [Deprecated], which will allow Elementa to no longer call it on newer versions.
131+
* If it is not annotated, Elementa will continue to call it and pay the corresponding performance penalty to do so.
132+
*
133+
* If your custom constraint is animated, use [Window.animationTimeNs]/[animationTimeMs][Window.animationTimeMs]
134+
* to drive that animation instead.
135+
*
136+
* You no longer need to call [SuperConstraint.animationFrame] to cause the cached value in a constraint to be
137+
* recomputed each frame. Constraints will now automatically register themselves with the [Window] they are
138+
* evaluated on, so it can invalidate them automatically via [Window.invalidateCachedConstraints].
139+
* This can be done manually any number of times during one frame and will be called by default at least twice per
140+
* frame (once before all update funcs and once after).
141+
*
142+
*
143+
* Additionally, given both new mechanisms are variable time, [Window.animationFPS] is now deprecated and the
144+
* meaning of any existing `frames` parameter which are used for timing and cannot be renamed without breaking ABI
145+
* (e.g. [AnimationComponent.elapsedFrames]) is changed to now mean "milliseconds" instead.
146+
*
147+
*
148+
* The main reasons for this change are:
149+
* - Previously it was not possible to get layout information from a component, then update it depending on that
150+
* information and still have that update be reflected in the current frame, because there was no safe way to
151+
* invalidate the cached layout information. You would either have to call `animationFrame` and accept some
152+
* animations running quicker than intended, or wait until the next frame.
153+
* - A common beginner mistake was to query layout information during `animationFrame`, during that method however
154+
* usually parts of the tree still have the old values cached, so evaluating the layout could result in those old
155+
* values being used while computing the new values. Now that the two operations are separate, it is safe to query
156+
* the layout during [UpdateFunc]s because `invalidateCachedConstraints` will be called again afterwards.
157+
* And if you change the layout in response to your measurements, you can call the method yourself to immediately
158+
* make visible those changes to all remaining [UpdateFunc]s too.
159+
* - Another common mistake was making changes to the component hierarchy from `animationFrame`. Given that method
160+
* is called from a trivial tree traversal, making changes to that tree could result in
161+
* ConcurrentModificationExceptions (or the custom "Cannot modify children while iterating over them." exception).
162+
* The [UpdateFunc] implementation does not suffer from this restriction.
163+
* - `animationFrame` runs on a fixed update rate, which almost certainly won't match the real frame rate perfectly
164+
* and will result in multiple calls per frame (by default 244 times per second), which not only wastes cpu time
165+
* but also results in slow motion animations if there isn't enough time for all the calls.
166+
* Since we mostly use this for animations, and not physics simulations, using variable rate updates is not really
167+
* any more difficult (in some cases it's actually easier) and solves both of these.
168+
* - `animationFrame` will traverse the entire tree, even if an entire branch has neither things that need regular
169+
* updates nor had its constraints evaluated (e.g. because it's off-screen).
170+
* The new constraint tracking will only invalidate constraints which were evaluated, and the [UpdateFunc]s
171+
* are tracked intelligently at registration, such that no more full tree traversals should be necessary.
172+
*/
173+
V8,
174+
101175
;
102176

103177
/**
@@ -142,7 +216,9 @@ Be sure to read through all the changes between your current version and your ne
142216
internal val v5 = V5
143217
@Suppress("DEPRECATION")
144218
internal val v6 = V6
219+
@Suppress("DEPRECATION")
145220
internal val v7 = V7
221+
internal val v8 = V8
146222

147223

148224
@PublishedApi

src/main/kotlin/gg/essential/elementa/UIComponent.kt

Lines changed: 125 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ abstract class UIComponent : Observable(), ReferenceHolder {
7979

8080
var constraints = UIConstraints(this)
8181
set(value) {
82+
(field as? AnimatingConstraints)?.updateFunc?.let { removeUpdateFunc(it) }
83+
if (value is AnimatingConstraints) {
84+
addUpdateFunc(object : UpdateFunc {
85+
override fun invoke(dt: Float, dtMs: Int) {
86+
if (Window.of(this@UIComponent).version < ElementaVersion.v8) {
87+
removeUpdateFunc(this) // handled by `animationFrame`
88+
return
89+
}
90+
value.updateCompletion(dtMs)
91+
}
92+
}.also { value.updateFunc = it })
93+
}
8294
field = value
8395
setChanged()
8496
notifyObservers(constraints)
@@ -139,6 +151,9 @@ abstract class UIComponent : Observable(), ReferenceHolder {
139151
private var didCallBeforeDraw = false
140152
private var warnedAboutBeforeDraw = false
141153

154+
internal val versionOrV0: ElementaVersion
155+
get() = Window.ofOrNull(this)?.version ?: ElementaVersion.v0
156+
142157
internal var cachedWindow: Window? = null
143158

144159
private fun setWindowCacheOnChangedChild(possibleEvent: Any) {
@@ -810,7 +825,38 @@ abstract class UIComponent : Observable(), ReferenceHolder {
810825
this.listener(typedChar, keyCode)
811826
}
812827

828+
@Deprecated("See [ElementaVersion.V8].")
813829
open fun animationFrame() {
830+
if (versionOrV0 >= ElementaVersion.v8) {
831+
doSparseAnimationFrame()
832+
} else {
833+
doLegacyAnimationFrame()
834+
}
835+
}
836+
837+
private fun doSparseAnimationFrame() {
838+
if (Flags.RequiresAnimationFrame in effectFlags) {
839+
for (effect in effects) {
840+
if (Flags.RequiresAnimationFrame in effect.flags) {
841+
@Suppress("DEPRECATION")
842+
effect.animationFrame()
843+
}
844+
}
845+
}
846+
for (child in children) {
847+
if (Flags.RequiresAnimationFrame in child.combinedFlags) {
848+
if (Flags.RequiresAnimationFrame in child.ownFlags) {
849+
@Suppress("DEPRECATION")
850+
child.animationFrame()
851+
} else {
852+
child.doSparseAnimationFrame()
853+
}
854+
}
855+
}
856+
}
857+
858+
@Suppress("DEPRECATION")
859+
private fun doLegacyAnimationFrame() {
814860
constraints.animationFrame()
815861

816862
effects.forEach(Effect::animationFrame)
@@ -825,14 +871,19 @@ abstract class UIComponent : Observable(), ReferenceHolder {
825871
}
826872

827873
// Process timers
828-
val timerIterator = activeTimers.iterator()
829-
timerIterator.forEachRemaining { (id, timer) ->
830-
if (id in stoppedTimers)
831-
return@forEachRemaining
832-
874+
updateTimers { timer ->
833875
val time = System.currentTimeMillis()
876+
if (timer.lastTime == -1L) timer.lastTime = time
834877
timer.timeLeft -= (time - timer.lastTime)
835878
timer.lastTime = time
879+
}
880+
}
881+
882+
private inline fun updateTimers(advance: (Timer) -> Unit) {
883+
for ((id, timer) in activeTimers) {
884+
if (id in stoppedTimers) continue
885+
886+
advance(timer)
836887

837888
if (!timer.hasDelayed && timer.timeLeft <= 0L) {
838889
timer.hasDelayed = true
@@ -1425,9 +1476,10 @@ abstract class UIComponent : Observable(), ReferenceHolder {
14251476
return
14261477
}
14271478

1428-
val totalFrames = (time * Window.of(this@UIComponent).animationFPS).toInt()
1429-
val totalDelay = (delay * Window.of(this@UIComponent).animationFPS).toInt()
1479+
val totalFrames = (time * Window.of(this@UIComponent).animationFPSOr1000).toInt()
1480+
val totalDelay = (delay * Window.of(this@UIComponent).animationFPSOr1000).toInt()
14301481

1482+
scheduleFieldAnimationUpdateFunc()
14311483
fieldAnimationQueue.removeIf { it.field == this }
14321484
fieldAnimationQueue.addFirst(
14331485
IntFieldAnimationComponent(
@@ -1450,9 +1502,10 @@ abstract class UIComponent : Observable(), ReferenceHolder {
14501502
return
14511503
}
14521504

1453-
val totalFrames = (time * Window.of(this@UIComponent).animationFPS).toInt()
1454-
val totalDelay = (delay * Window.of(this@UIComponent).animationFPS).toInt()
1505+
val totalFrames = (time * Window.of(this@UIComponent).animationFPSOr1000).toInt()
1506+
val totalDelay = (delay * Window.of(this@UIComponent).animationFPSOr1000).toInt()
14551507

1508+
scheduleFieldAnimationUpdateFunc()
14561509
fieldAnimationQueue.removeIf { it.field == this }
14571510
fieldAnimationQueue.addFirst(
14581511
FloatFieldAnimationComponent(
@@ -1475,9 +1528,10 @@ abstract class UIComponent : Observable(), ReferenceHolder {
14751528
return
14761529
}
14771530

1478-
val totalFrames = (time * Window.of(this@UIComponent).animationFPS).toInt()
1479-
val totalDelay = (delay * Window.of(this@UIComponent).animationFPS).toInt()
1531+
val totalFrames = (time * Window.of(this@UIComponent).animationFPSOr1000).toInt()
1532+
val totalDelay = (delay * Window.of(this@UIComponent).animationFPSOr1000).toInt()
14801533

1534+
scheduleFieldAnimationUpdateFunc()
14811535
fieldAnimationQueue.removeIf { it.field == this }
14821536
fieldAnimationQueue.addFirst(
14831537
LongFieldAnimationComponent(
@@ -1505,9 +1559,10 @@ abstract class UIComponent : Observable(), ReferenceHolder {
15051559
return
15061560
}
15071561

1508-
val totalFrames = (time * Window.of(this@UIComponent).animationFPS).toInt()
1509-
val totalDelay = (delay * Window.of(this@UIComponent).animationFPS).toInt()
1562+
val totalFrames = (time * Window.of(this@UIComponent).animationFPSOr1000).toInt()
1563+
val totalDelay = (delay * Window.of(this@UIComponent).animationFPSOr1000).toInt()
15101564

1565+
scheduleFieldAnimationUpdateFunc()
15111566
fieldAnimationQueue.removeIf { it.field == this }
15121567
fieldAnimationQueue.addFirst(
15131568
DoubleFieldAnimationComponent(
@@ -1530,9 +1585,10 @@ abstract class UIComponent : Observable(), ReferenceHolder {
15301585
return
15311586
}
15321587

1533-
val totalFrames = (time * Window.of(this@UIComponent).animationFPS).toInt()
1534-
val totalDelay = (delay * Window.of(this@UIComponent).animationFPS).toInt()
1588+
val totalFrames = (time * Window.of(this@UIComponent).animationFPSOr1000).toInt()
1589+
val totalDelay = (delay * Window.of(this@UIComponent).animationFPSOr1000).toInt()
15351590

1591+
scheduleFieldAnimationUpdateFunc()
15361592
fieldAnimationQueue.removeIf { it.field == this }
15371593
fieldAnimationQueue.addFirst(
15381594
ColorFieldAnimationComponent(
@@ -1550,6 +1606,36 @@ abstract class UIComponent : Observable(), ReferenceHolder {
15501606
fieldAnimationQueue.removeIf { it.field == this }
15511607
}
15521608

1609+
private fun scheduleFieldAnimationUpdateFunc() {
1610+
if (fieldAnimationQueue.isNotEmpty()) return // should already be scheduled
1611+
1612+
addUpdateFunc(object : UpdateFunc {
1613+
override fun invoke(dt: Float, dtMs: Int) {
1614+
if (Window.of(this@UIComponent).version < ElementaVersion.v8) {
1615+
// Field animations will be handled via `animationFrame`
1616+
removeUpdateFunc(this)
1617+
return
1618+
}
1619+
1620+
val queueIterator = fieldAnimationQueue.iterator()
1621+
queueIterator.forEachRemaining { anim ->
1622+
if (!anim.animationPaused) {
1623+
anim.elapsedFrames += dtMs
1624+
}
1625+
anim.setValue(anim.getPercentComplete())
1626+
1627+
if (anim.isComplete()) {
1628+
queueIterator.remove()
1629+
}
1630+
}
1631+
1632+
if (fieldAnimationQueue.isEmpty()) {
1633+
removeUpdateFunc(this)
1634+
}
1635+
}
1636+
})
1637+
}
1638+
15531639
private fun validateAnimationFields(time: Float, delay: Float): Boolean {
15541640
if (time < 0f) {
15551641
println("time parameter of field animation call cannot be less than 0")
@@ -1578,6 +1664,7 @@ abstract class UIComponent : Observable(), ReferenceHolder {
15781664
*/
15791665

15801666
fun startTimer(interval: Long, delay: Long = 0, callback: (Int) -> Unit): Int {
1667+
scheduleTimerUpdateFunc()
15811668
val id = nextTimerId++
15821669
activeTimers[id] = Timer(delay, interval, callback)
15831670
return id
@@ -1607,7 +1694,7 @@ abstract class UIComponent : Observable(), ReferenceHolder {
16071694
private class Timer(delay: Long, val interval: Long, val callback: (Int) -> Unit) {
16081695
var hasDelayed = false
16091696
var timeLeft = delay
1610-
var lastTime = System.currentTimeMillis()
1697+
var lastTime: Long = -1 // used only with `animationFrame` / pre-v8
16111698

16121699
init {
16131700
if (delay == 0L) {
@@ -1617,6 +1704,26 @@ abstract class UIComponent : Observable(), ReferenceHolder {
16171704
}
16181705
}
16191706

1707+
private fun scheduleTimerUpdateFunc() {
1708+
if (activeTimers.isNotEmpty()) return // should already be scheduled
1709+
1710+
addUpdateFunc(object : UpdateFunc {
1711+
override fun invoke(dt: Float, dtMs: Int) {
1712+
if (Window.of(this@UIComponent).version < ElementaVersion.v8) {
1713+
// Timers will be handled via `animationFrame`
1714+
removeUpdateFunc(this)
1715+
return
1716+
}
1717+
1718+
updateTimers { it.timeLeft -= dtMs }
1719+
1720+
if (activeTimers.isEmpty()) {
1721+
removeUpdateFunc(this)
1722+
}
1723+
}
1724+
})
1725+
}
1726+
16201727
override fun holdOnto(listener: Any): () -> Unit {
16211728
heldReferences.add(listener)
16221729
return { heldReferences.remove(listener) }
@@ -1640,6 +1747,7 @@ abstract class UIComponent : Observable(), ReferenceHolder {
16401747

16411748
val RequiresMouseMove = iota
16421749
val RequiresMouseDrag = iota
1750+
val RequiresAnimationFrame = iota // only applies when ElementaVersion >= V8
16431751

16441752
val All = Flags(iota.bits - 1u)
16451753

@@ -1656,6 +1764,7 @@ abstract class UIComponent : Observable(), ReferenceHolder {
16561764
if (cls.overridesMethod("mouseMove", Window::class.java)) RequiresMouseMove else None,
16571765
if (cls.overridesMethod("dragMouse", Int::class.java, Int::class.java, Int::class.java)) RequiresMouseDrag else None,
16581766
if (cls.overridesMethod("dragMouse", Float::class.java, Float::class.java, Int::class.java)) RequiresMouseDrag else None,
1767+
if (cls.overridesMethod("animationFrame")) RequiresAnimationFrame else None,
16591768
).reduce { acc, flags -> acc + flags }
16601769

16611770
private fun Class<*>.overridesMethod(name: String, vararg args: Class<*>) =

0 commit comments

Comments
 (0)