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