diff --git a/api/Elementa.api b/api/Elementa.api index 82c967cf..087b0adb 100644 --- a/api/Elementa.api +++ b/api/Elementa.api @@ -8,6 +8,7 @@ public final class gg/essential/elementa/ElementaVersion : java/lang/Enum { public static final field V5 Lgg/essential/elementa/ElementaVersion; public static final field V6 Lgg/essential/elementa/ElementaVersion; public static final field V7 Lgg/essential/elementa/ElementaVersion; + public static final field V8 Lgg/essential/elementa/ElementaVersion; public final fun enableFor (Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; public static fun valueOf (Ljava/lang/String;)Lgg/essential/elementa/ElementaVersion; public static fun values ()[Lgg/essential/elementa/ElementaVersion; @@ -362,7 +363,6 @@ public final class gg/essential/elementa/components/ScrollComponent : gg/essenti public synthetic fun addChild (Lgg/essential/elementa/UIComponent;)Lgg/essential/elementa/UIComponent; public fun addChild (Lgg/essential/elementa/UIComponent;)Lgg/essential/elementa/components/ScrollComponent; public final fun addScrollAdjustEvent (ZLkotlin/jvm/functions/Function2;)V - public fun afterInitialization ()V public fun alwaysDrawChildren ()Z public fun animationFrame ()V public fun childrenOfType (Ljava/lang/Class;)Ljava/util/List; @@ -756,6 +756,8 @@ public final class gg/essential/elementa/components/Window : gg/essential/elemen public final fun drawFloatingComponents (Lgg/essential/universal/UMatrixStack;)V public final fun focus (Lgg/essential/elementa/UIComponent;)V public final fun getAnimationFPS ()I + public final fun getAnimationTimeMs ()J + public final fun getAnimationTimeNs ()J public fun getBottom ()F public final fun getFocusedComponent ()Lgg/essential/elementa/UIComponent; public final fun getHasErrored ()Z @@ -766,6 +768,7 @@ public final class gg/essential/elementa/components/Window : gg/essential/elemen public fun getTop ()F public fun getWidth ()F public fun hitTest (FF)Lgg/essential/elementa/UIComponent; + public final fun invalidateCachedConstraints ()V public final fun isAreaVisible (DDDD)Z public fun keyType (CI)V public fun mouseClick (DDI)V @@ -1871,7 +1874,6 @@ public final class gg/essential/elementa/constraints/RainbowColorConstraint : gg public fun ()V public fun (IF)V public synthetic fun (IFILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun animationFrame ()V public final fun getAlpha ()I public fun getCachedValue ()Ljava/awt/Color; public synthetic fun getCachedValue ()Ljava/lang/Object; diff --git a/src/main/kotlin/gg/essential/elementa/ElementaVersion.kt b/src/main/kotlin/gg/essential/elementa/ElementaVersion.kt index e63c2687..8df41a71 100644 --- a/src/main/kotlin/gg/essential/elementa/ElementaVersion.kt +++ b/src/main/kotlin/gg/essential/elementa/ElementaVersion.kt @@ -1,5 +1,11 @@ package gg.essential.elementa +import gg.essential.elementa.components.UpdateFunc +import gg.essential.elementa.components.Window +import gg.essential.elementa.constraints.SuperConstraint +import gg.essential.elementa.constraints.animation.AnimationComponent +import gg.essential.elementa.effects.Effect + /** * Sometimes it is necessary or desirable to introduce breaking behavioral changes to Elementa. In order to maintain * full backwards compatibility in these cases, library consumers must explicitly opt-in to such changes for their @@ -96,8 +102,76 @@ enum class ElementaVersion { /** * [gg.essential.elementa.components.Window] now disables input events if an error has occurred during drawing. */ + @Deprecated(DEPRECATION_MESSAGE) V7, + /** + * The [animationFrame][UIComponent.animationFrame] methods are now deprecated and will no longer be called at all + * for [constraints][SuperConstraint.animationFrame] or if your override is marked as [Deprecated]. + * The relative order in which various things ([UpdateFunc]s, constraint cache invalidation, [UIComponent] timers + * and field animations, [UIComponent.animationFrame], and [Effect.animationFrame]) will be called has changed + * because constraint cache invalidation is separate now and [UpdateFunc]s are used internally for timers and field + * animations now. + * + * All custom constraints which currently rely on `animationFrame` must be updated to support the new + * `animationTime` mechanism described below before this version can be enabled! + * + * All custom components and effects which override `animationFrame` should be updated to use the [UpdateFunc] API + * instead, and some may also require updates to account for the change in relative order mentioned above. + * Note however that both the UpdateFunc mechanism and the animationTime properties are both available on any + * [ElementaVersion], so most (if not all) of your components can migrate to them even before opting to enable + * this [ElementaVersion]. + * + * If your custom component or effect needs to update some animation or other miscellaneous state before each frame, + * use the [UpdateFunc] mechanism instead (via [UIComponent.addUpdateFunc]/[Effect.addUpdateFunc]). + * This way, only components which actually have something that needs updating will need to be called each frame. + * + * If your custom component or effect needs to continue to support older [ElementaVersion]s, ideally mark your + * `animationFrame` override as [Deprecated], which will allow Elementa to no longer call it on newer versions. + * If it is not annotated, Elementa will continue to call it and pay the corresponding performance penalty to do so. + * + * If your custom constraint is animated, use [Window.animationTimeNs]/[animationTimeMs][Window.animationTimeMs] + * to drive that animation instead. + * + * You no longer need to call [SuperConstraint.animationFrame] to cause the cached value in a constraint to be + * recomputed each frame. Constraints will now automatically register themselves with the [Window] they are + * evaluated on, so it can invalidate them automatically via [Window.invalidateCachedConstraints]. + * This can be done manually any number of times during one frame and will be called by default at least twice per + * frame (once before all update funcs and once after). + * + * + * Additionally, given both new mechanisms are variable time, [Window.animationFPS] is now deprecated and the + * meaning of any existing `frames` parameter which are used for timing and cannot be renamed without breaking ABI + * (e.g. [AnimationComponent.elapsedFrames]) is changed to now mean "milliseconds" instead. + * + * + * The main reasons for this change are: + * - Previously it was not possible to get layout information from a component, then update it depending on that + * information and still have that update be reflected in the current frame, because there was no safe way to + * invalidate the cached layout information. You would either have to call `animationFrame` and accept some + * animations running quicker than intended, or wait until the next frame. + * - A common beginner mistake was to query layout information during `animationFrame`, during that method however + * usually parts of the tree still have the old values cached, so evaluating the layout could result in those old + * values being used while computing the new values. Now that the two operations are separate, it is safe to query + * the layout during [UpdateFunc]s because `invalidateCachedConstraints` will be called again afterwards. + * And if you change the layout in response to your measurements, you can call the method yourself to immediately + * make visible those changes to all remaining [UpdateFunc]s too. + * - Another common mistake was making changes to the component hierarchy from `animationFrame`. Given that method + * is called from a trivial tree traversal, making changes to that tree could result in + * ConcurrentModificationExceptions (or the custom "Cannot modify children while iterating over them." exception). + * The [UpdateFunc] implementation does not suffer from this restriction. + * - `animationFrame` runs on a fixed update rate, which almost certainly won't match the real frame rate perfectly + * and will result in multiple calls per frame (by default 244 times per second), which not only wastes cpu time + * but also results in slow motion animations if there isn't enough time for all the calls. + * Since we mostly use this for animations, and not physics simulations, using variable rate updates is not really + * any more difficult (in some cases it's actually easier) and solves both of these. + * - `animationFrame` will traverse the entire tree, even if an entire branch has neither things that need regular + * updates nor had its constraints evaluated (e.g. because it's off-screen). + * The new constraint tracking will only invalidate constraints which were evaluated, and the [UpdateFunc]s + * are tracked intelligently at registration, such that no more full tree traversals should be necessary. + */ + V8, + ; /** @@ -142,7 +216,9 @@ Be sure to read through all the changes between your current version and your ne internal val v5 = V5 @Suppress("DEPRECATION") internal val v6 = V6 + @Suppress("DEPRECATION") internal val v7 = V7 + internal val v8 = V8 @PublishedApi diff --git a/src/main/kotlin/gg/essential/elementa/UIComponent.kt b/src/main/kotlin/gg/essential/elementa/UIComponent.kt index a4083dd6..b9ee78d1 100644 --- a/src/main/kotlin/gg/essential/elementa/UIComponent.kt +++ b/src/main/kotlin/gg/essential/elementa/UIComponent.kt @@ -79,6 +79,18 @@ abstract class UIComponent : Observable(), ReferenceHolder { var constraints = UIConstraints(this) set(value) { + (field as? AnimatingConstraints)?.updateFunc?.let { removeUpdateFunc(it) } + if (value is AnimatingConstraints) { + addUpdateFunc(object : UpdateFunc { + override fun invoke(dt: Float, dtMs: Int) { + if (Window.of(this@UIComponent).version < ElementaVersion.v8) { + removeUpdateFunc(this) // handled by `animationFrame` + return + } + value.updateCompletion(dtMs) + } + }.also { value.updateFunc = it }) + } field = value setChanged() notifyObservers(constraints) @@ -139,6 +151,9 @@ abstract class UIComponent : Observable(), ReferenceHolder { private var didCallBeforeDraw = false private var warnedAboutBeforeDraw = false + internal val versionOrV0: ElementaVersion + get() = Window.ofOrNull(this)?.version ?: ElementaVersion.v0 + internal var cachedWindow: Window? = null private fun setWindowCacheOnChangedChild(possibleEvent: Any) { @@ -810,7 +825,38 @@ abstract class UIComponent : Observable(), ReferenceHolder { this.listener(typedChar, keyCode) } + @Deprecated("See [ElementaVersion.V8].") open fun animationFrame() { + if (versionOrV0 >= ElementaVersion.v8) { + doSparseAnimationFrame() + } else { + doLegacyAnimationFrame() + } + } + + private fun doSparseAnimationFrame() { + if (Flags.RequiresAnimationFrame in effectFlags) { + for (effect in effects) { + if (Flags.RequiresAnimationFrame in effect.flags) { + @Suppress("DEPRECATION") + effect.animationFrame() + } + } + } + for (child in children) { + if (Flags.RequiresAnimationFrame in child.combinedFlags) { + if (Flags.RequiresAnimationFrame in child.ownFlags) { + @Suppress("DEPRECATION") + child.animationFrame() + } else { + child.doSparseAnimationFrame() + } + } + } + } + + @Suppress("DEPRECATION") + private fun doLegacyAnimationFrame() { constraints.animationFrame() effects.forEach(Effect::animationFrame) @@ -825,14 +871,19 @@ abstract class UIComponent : Observable(), ReferenceHolder { } // Process timers - val timerIterator = activeTimers.iterator() - timerIterator.forEachRemaining { (id, timer) -> - if (id in stoppedTimers) - return@forEachRemaining - + updateTimers { timer -> val time = System.currentTimeMillis() + if (timer.lastTime == -1L) timer.lastTime = time timer.timeLeft -= (time - timer.lastTime) timer.lastTime = time + } + } + + private inline fun updateTimers(advance: (Timer) -> Unit) { + for ((id, timer) in activeTimers) { + if (id in stoppedTimers) continue + + advance(timer) if (!timer.hasDelayed && timer.timeLeft <= 0L) { timer.hasDelayed = true @@ -1425,9 +1476,10 @@ abstract class UIComponent : Observable(), ReferenceHolder { return } - val totalFrames = (time * Window.of(this@UIComponent).animationFPS).toInt() - val totalDelay = (delay * Window.of(this@UIComponent).animationFPS).toInt() + val totalFrames = (time * Window.of(this@UIComponent).animationFPSOr1000).toInt() + val totalDelay = (delay * Window.of(this@UIComponent).animationFPSOr1000).toInt() + scheduleFieldAnimationUpdateFunc() fieldAnimationQueue.removeIf { it.field == this } fieldAnimationQueue.addFirst( IntFieldAnimationComponent( @@ -1450,9 +1502,10 @@ abstract class UIComponent : Observable(), ReferenceHolder { return } - val totalFrames = (time * Window.of(this@UIComponent).animationFPS).toInt() - val totalDelay = (delay * Window.of(this@UIComponent).animationFPS).toInt() + val totalFrames = (time * Window.of(this@UIComponent).animationFPSOr1000).toInt() + val totalDelay = (delay * Window.of(this@UIComponent).animationFPSOr1000).toInt() + scheduleFieldAnimationUpdateFunc() fieldAnimationQueue.removeIf { it.field == this } fieldAnimationQueue.addFirst( FloatFieldAnimationComponent( @@ -1475,9 +1528,10 @@ abstract class UIComponent : Observable(), ReferenceHolder { return } - val totalFrames = (time * Window.of(this@UIComponent).animationFPS).toInt() - val totalDelay = (delay * Window.of(this@UIComponent).animationFPS).toInt() + val totalFrames = (time * Window.of(this@UIComponent).animationFPSOr1000).toInt() + val totalDelay = (delay * Window.of(this@UIComponent).animationFPSOr1000).toInt() + scheduleFieldAnimationUpdateFunc() fieldAnimationQueue.removeIf { it.field == this } fieldAnimationQueue.addFirst( LongFieldAnimationComponent( @@ -1505,9 +1559,10 @@ abstract class UIComponent : Observable(), ReferenceHolder { return } - val totalFrames = (time * Window.of(this@UIComponent).animationFPS).toInt() - val totalDelay = (delay * Window.of(this@UIComponent).animationFPS).toInt() + val totalFrames = (time * Window.of(this@UIComponent).animationFPSOr1000).toInt() + val totalDelay = (delay * Window.of(this@UIComponent).animationFPSOr1000).toInt() + scheduleFieldAnimationUpdateFunc() fieldAnimationQueue.removeIf { it.field == this } fieldAnimationQueue.addFirst( DoubleFieldAnimationComponent( @@ -1530,9 +1585,10 @@ abstract class UIComponent : Observable(), ReferenceHolder { return } - val totalFrames = (time * Window.of(this@UIComponent).animationFPS).toInt() - val totalDelay = (delay * Window.of(this@UIComponent).animationFPS).toInt() + val totalFrames = (time * Window.of(this@UIComponent).animationFPSOr1000).toInt() + val totalDelay = (delay * Window.of(this@UIComponent).animationFPSOr1000).toInt() + scheduleFieldAnimationUpdateFunc() fieldAnimationQueue.removeIf { it.field == this } fieldAnimationQueue.addFirst( ColorFieldAnimationComponent( @@ -1550,6 +1606,36 @@ abstract class UIComponent : Observable(), ReferenceHolder { fieldAnimationQueue.removeIf { it.field == this } } + private fun scheduleFieldAnimationUpdateFunc() { + if (fieldAnimationQueue.isNotEmpty()) return // should already be scheduled + + addUpdateFunc(object : UpdateFunc { + override fun invoke(dt: Float, dtMs: Int) { + if (Window.of(this@UIComponent).version < ElementaVersion.v8) { + // Field animations will be handled via `animationFrame` + removeUpdateFunc(this) + return + } + + val queueIterator = fieldAnimationQueue.iterator() + queueIterator.forEachRemaining { anim -> + if (!anim.animationPaused) { + anim.elapsedFrames += dtMs + } + anim.setValue(anim.getPercentComplete()) + + if (anim.isComplete()) { + queueIterator.remove() + } + } + + if (fieldAnimationQueue.isEmpty()) { + removeUpdateFunc(this) + } + } + }) + } + private fun validateAnimationFields(time: Float, delay: Float): Boolean { if (time < 0f) { println("time parameter of field animation call cannot be less than 0") @@ -1578,6 +1664,7 @@ abstract class UIComponent : Observable(), ReferenceHolder { */ fun startTimer(interval: Long, delay: Long = 0, callback: (Int) -> Unit): Int { + scheduleTimerUpdateFunc() val id = nextTimerId++ activeTimers[id] = Timer(delay, interval, callback) return id @@ -1607,7 +1694,7 @@ abstract class UIComponent : Observable(), ReferenceHolder { private class Timer(delay: Long, val interval: Long, val callback: (Int) -> Unit) { var hasDelayed = false var timeLeft = delay - var lastTime = System.currentTimeMillis() + var lastTime: Long = -1 // used only with `animationFrame` / pre-v8 init { if (delay == 0L) { @@ -1617,6 +1704,26 @@ abstract class UIComponent : Observable(), ReferenceHolder { } } + private fun scheduleTimerUpdateFunc() { + if (activeTimers.isNotEmpty()) return // should already be scheduled + + addUpdateFunc(object : UpdateFunc { + override fun invoke(dt: Float, dtMs: Int) { + if (Window.of(this@UIComponent).version < ElementaVersion.v8) { + // Timers will be handled via `animationFrame` + removeUpdateFunc(this) + return + } + + updateTimers { it.timeLeft -= dtMs } + + if (activeTimers.isEmpty()) { + removeUpdateFunc(this) + } + } + }) + } + override fun holdOnto(listener: Any): () -> Unit { heldReferences.add(listener) return { heldReferences.remove(listener) } @@ -1640,6 +1747,7 @@ abstract class UIComponent : Observable(), ReferenceHolder { val RequiresMouseMove = iota val RequiresMouseDrag = iota + val RequiresAnimationFrame = iota // only applies when ElementaVersion >= V8 val All = Flags(iota.bits - 1u) @@ -1656,6 +1764,7 @@ abstract class UIComponent : Observable(), ReferenceHolder { if (cls.overridesMethod("mouseMove", Window::class.java)) RequiresMouseMove else None, if (cls.overridesMethod("dragMouse", Int::class.java, Int::class.java, Int::class.java)) RequiresMouseDrag else None, if (cls.overridesMethod("dragMouse", Float::class.java, Float::class.java, Int::class.java)) RequiresMouseDrag else None, + if (cls.overridesMethod("animationFrame")) RequiresAnimationFrame else None, ).reduce { acc, flags -> acc + flags } private fun Class<*>.overridesMethod(name: String, vararg args: Class<*>) = diff --git a/src/main/kotlin/gg/essential/elementa/UIConstraints.kt b/src/main/kotlin/gg/essential/elementa/UIConstraints.kt index 5c858c8f..10f1cea8 100644 --- a/src/main/kotlin/gg/essential/elementa/UIConstraints.kt +++ b/src/main/kotlin/gg/essential/elementa/UIConstraints.kt @@ -111,6 +111,8 @@ open class UIConstraints(protected val component: UIComponent) : Observable() { color = constraint } + @Deprecated("See [ElementaVersion.V8].") + @Suppress("DEPRECATION") internal open fun animationFrame() { x.animationFrame() y.animationFrame() diff --git a/src/main/kotlin/gg/essential/elementa/components/ScrollComponent.kt b/src/main/kotlin/gg/essential/elementa/components/ScrollComponent.kt index d67c829c..3caff34f 100644 --- a/src/main/kotlin/gg/essential/elementa/components/ScrollComponent.kt +++ b/src/main/kotlin/gg/essential/elementa/components/ScrollComponent.kt @@ -2,6 +2,7 @@ package gg.essential.elementa.components import gg.essential.elementa.ElementaVersion import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.UpdateFunc import gg.essential.elementa.constraints.* import gg.essential.elementa.constraints.animation.Animations import gg.essential.elementa.constraints.resolution.ConstraintVisitor @@ -78,8 +79,6 @@ class ScrollComponent constructor( private val verticalScrollEnabled get() = primaryScrollDirection == Direction.Vertical || secondaryScrollDirection == Direction.Vertical - private var animationFPS: Int? = null - private val actualHolder = UIContainer().constrain { x = innerPadding.pixels() y = innerPadding.pixels() @@ -233,12 +232,6 @@ class ScrollComponent constructor( super.draw(matrixStack) } - override fun afterInitialization() { - super.afterInitialization() - - animationFPS = Window.of(this).animationFPS - } - /** * Sets the text that appears when no items are shown */ @@ -551,11 +544,18 @@ class ScrollComponent constructor( } } + init { addUpdateFuncOnV8ReplacingAnimationFrame { dt, _ -> doUpdate(dt) } } + @Deprecated("See [ElementaVersion.V8].") + @Suppress("DEPRECATION") override fun animationFrame() { super.animationFrame() + if (versionOrV0 >= ElementaVersion.v8) return // handled by UpdateFunc + doUpdate(1f / (Window.ofOrNull(this)?.animationFPS ?: 244)) + } + private fun doUpdate(dt: Float) { currentScrollAcceleration = - (currentScrollAcceleration - ((scrollAcceleration - 1.0f) / (animationFPS ?: 244).toFloat())) + (currentScrollAcceleration - (scrollAcceleration - 1.0f) * dt) .coerceAtLeast(1.0f) if (!isAutoScrolling) return @@ -567,7 +567,7 @@ class ScrollComponent constructor( if (currentX in getLeft()..getRight()) { val deltaX = currentX - xBegin val percentX = deltaX / (-getWidth() / 2) - horizontalOffset += (percentX.toFloat() * 5f) + horizontalOffset += (percentX.toFloat() * 5f * 244 * dt) needsUpdate = true } } @@ -579,7 +579,7 @@ class ScrollComponent constructor( if (currentY in getTop()..getBottom()) { val deltaY = currentY - yBegin val percentY = deltaY / (-getHeight() / 2) - verticalOffset += (percentY.toFloat() * 5f) + verticalOffset += (percentY.toFloat() * 5f * 244 * dt) needsUpdate = true } } @@ -820,6 +820,7 @@ class ScrollComponent constructor( override var recalculate: Boolean = true override var constrainTo: UIComponent? = null + @Suppress("OVERRIDE_DEPRECATION", "DEPRECATION") override fun animationFrame() { super.animationFrame() desiredSize.animationFrame() diff --git a/src/main/kotlin/gg/essential/elementa/components/Window.kt b/src/main/kotlin/gg/essential/elementa/components/Window.kt index bb14554c..b9da3d76 100644 --- a/src/main/kotlin/gg/essential/elementa/components/Window.kt +++ b/src/main/kotlin/gg/essential/elementa/components/Window.kt @@ -2,6 +2,7 @@ package gg.essential.elementa.components import gg.essential.elementa.ElementaVersion import gg.essential.elementa.UIComponent +import gg.essential.elementa.constraints.SuperConstraint import gg.essential.elementa.constraints.resolution.ConstraintResolutionGui import gg.essential.elementa.constraints.resolution.ConstraintResolver import gg.essential.elementa.constraints.resolution.ConstraintResolverV2 @@ -20,15 +21,21 @@ import java.util.concurrent.TimeUnit */ class Window @JvmOverloads constructor( internal val version: ElementaVersion, + @Deprecated("See [ElementaVersion.V8].") val animationFPS: Int = 244 ) : UIComponent() { - private var systemTime = -1L - + private var legacyAnimationFrameTime = -1L private var lastDrawTime: Long = -1 + var animationTimeNs: Long = 0 + private set + val animationTimeMs: Long + get() = animationTimeNs / 1_000_000 internal var allUpdateFuncs: MutableList = mutableListOf() internal var nextUpdateFuncIndex = 0 + internal val cachedConstraints: MutableList> = mutableListOf() + private var currentMouseButton = -1 private var legacyFloatingComponents = mutableListOf() @@ -79,8 +86,8 @@ class Window @JvmOverloads constructor( it.remove() } - if (systemTime == -1L) - systemTime = System.currentTimeMillis() + if (legacyAnimationFrameTime == -1L) + legacyAnimationFrameTime = System.currentTimeMillis() if (lastDrawTime == -1L) lastDrawTime = System.currentTimeMillis() @@ -88,8 +95,20 @@ class Window @JvmOverloads constructor( val dtMs = now - lastDrawTime lastDrawTime = now + animationTimeNs += dtMs * 1_000_000 + try { + if (version >= ElementaVersion.v8) { + dispatchMouseDragging() + + if (componentRequestingFocus != null) { + dealWithFocusRequests() + } + + invalidateCachedConstraints() + } + assertUpdateFuncInvariants() nextUpdateFuncIndex = 0 while (true) { @@ -98,14 +117,20 @@ class Window @JvmOverloads constructor( func(dtMs / 1000f, dtMs.toInt()) } + if (version >= ElementaVersion.v8) { + invalidateCachedConstraints() + } + //If this Window is more than 5 seconds behind, reset it be only 5 seconds. //This will drop missed frames but avoid the game freezing as the Window tries //to catch after a period of inactivity - if (System.currentTimeMillis() - this.systemTime > TimeUnit.SECONDS.toMillis(5)) - this.systemTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(5) + if (System.currentTimeMillis() - this.legacyAnimationFrameTime > TimeUnit.SECONDS.toMillis(5)) + this.legacyAnimationFrameTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(5) + @Suppress("DEPRECATION") + val animationFPS = animationFPS val target = System.currentTimeMillis() + 1000 / animationFPS - val animationFrames = (target - this.systemTime).toInt() * animationFPS / 1000 + val animationFrames = (target - this.legacyAnimationFrameTime).toInt() * animationFPS / 1000 // If the window is sufficiently complex, it's possible for the average `animationFrame` to take so long // we'll start falling behind with no way to ever catch up. And the amount of frames we're behind will // quickly grow to the point where we'll be spending five seconds in `animationFrame` before we can get a @@ -113,8 +138,13 @@ class Window @JvmOverloads constructor( // To prevent that, we limit the `animationFrame` calls we make per real frame such that we'll still be able // to render approximately 30 real frames per second at the cost of animations slowing down. repeat(animationFrames.coerceAtMost((animationFPS / 30).coerceAtLeast(1))) { + @Suppress("DEPRECATION") animationFrame() - this.systemTime += 1000 / animationFPS + this.legacyAnimationFrameTime += 1000 / animationFPS + + if (version >= ElementaVersion.v8) { + invalidateCachedConstraints() + } } hoveredFloatingComponent = null @@ -300,7 +330,7 @@ class Window @JvmOverloads constructor( internal var prevDraggedMouseX: Float? = null internal var prevDraggedMouseY: Float? = null - override fun animationFrame() { + private fun dispatchMouseDragging() { if (currentMouseButton != -1) { val (mouseX, mouseY) = getMousePosition() if (version >= ElementaVersion.v2) { @@ -318,6 +348,21 @@ class Window @JvmOverloads constructor( } } } + } + + @Deprecated("See [ElementaVersion.V8].") + override fun animationFrame() { + if (version >= ElementaVersion.v8) { + // In v8, dragging and focus is handled before the UpdateFunc calls, we only need to call super to support + // components or effects which may still use animationFrame. + if (Flags.RequiresAnimationFrame in combinedFlags) { + @Suppress("DEPRECATION") + super.animationFrame() + } + return + } + + dispatchMouseDragging() if (componentRequestingFocus != null && componentRequestingFocus != focusedComponent) { if (focusedComponent != null) @@ -328,9 +373,19 @@ class Window @JvmOverloads constructor( } componentRequestingFocus = null + @Suppress("DEPRECATION") super.animationFrame() } + // Note: Constraints are cached this way only with ElementaVersion.V8 and above, + // prior version require calling `animationFrame` which may have additional side-effects. + fun invalidateCachedConstraints() { + for (constraint in cachedConstraints) { + constraint.recalculate = true + } + cachedConstraints.clear() + } + override fun getLeft(): Float { return 0f } @@ -443,6 +498,10 @@ class Window @JvmOverloads constructor( focusedComponent = null } + @Suppress("DEPRECATION") + internal val animationFPSOr1000: Int + get() = if (version >= ElementaVersion.v8) 1000 else animationFPS + companion object { private val renderOperations = ConcurrentLinkedQueue<() -> Unit>() diff --git a/src/main/kotlin/gg/essential/elementa/components/input/AbstractTextInput.kt b/src/main/kotlin/gg/essential/elementa/components/input/AbstractTextInput.kt index 7aff6519..9f85f7ee 100644 --- a/src/main/kotlin/gg/essential/elementa/components/input/AbstractTextInput.kt +++ b/src/main/kotlin/gg/essential/elementa/components/input/AbstractTextInput.kt @@ -1,7 +1,11 @@ package gg.essential.elementa.components.input +import gg.essential.elementa.ElementaVersion import gg.essential.elementa.UIComponent import gg.essential.elementa.components.UIBlock +import gg.essential.elementa.components.UpdateFunc +import gg.essential.elementa.components.Window +import gg.essential.elementa.components.addUpdateFuncOnV8ReplacingAnimationFrame import gg.essential.elementa.constraints.CenterConstraint import gg.essential.elementa.constraints.animation.Animations import gg.essential.elementa.dsl.* @@ -738,9 +742,16 @@ abstract class AbstractTextInput( } } + init { addUpdateFuncOnV8ReplacingAnimationFrame { _, _ -> update() } } + @Deprecated("See [ElementaVersion.V8].") override fun animationFrame() { + @Suppress("DEPRECATION") super.animationFrame() + if (versionOrV0 >= ElementaVersion.v8) return // handled by UpdateFunc + update() + } + private fun update() { val diff = (targetVerticalScrollingOffset - verticalScrollingOffset) * 0.1f if (abs(diff) < .25f) verticalScrollingOffset = targetVerticalScrollingOffset diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/InfoBlockNode.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/InfoBlockNode.kt index 80a245e7..b38d0158 100644 --- a/src/main/kotlin/gg/essential/elementa/components/inspector/InfoBlockNode.kt +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/InfoBlockNode.kt @@ -1,9 +1,11 @@ package gg.essential.elementa.components.inspector +import gg.essential.elementa.ElementaVersion import gg.essential.elementa.UIComponent import gg.essential.elementa.components.TreeNode import gg.essential.elementa.components.UIContainer import gg.essential.elementa.components.UIText +import gg.essential.elementa.components.Window import gg.essential.elementa.constraints.* import gg.essential.elementa.constraints.animation.AnimationComponent import gg.essential.elementa.dsl.childOf @@ -75,9 +77,11 @@ class InfoBlockNode(private val constraint: SuperConstraint, private val n } childOf stringHolder } - override fun animationFrame() { - super.animationFrame() + init { + addUpdateFunc { _, _ -> update() } + } + private fun update() { if (constraint is AnimationComponent<*>) { val strings = stringHolder.childrenOfType() val percentComplete = constraint.elapsedFrames.toFloat() / (constraint.totalFrames + constraint.delayFrames) diff --git a/src/main/kotlin/gg/essential/elementa/components/updateFunc.kt b/src/main/kotlin/gg/essential/elementa/components/updateFunc.kt index 7c6482fa..7c063a32 100644 --- a/src/main/kotlin/gg/essential/elementa/components/updateFunc.kt +++ b/src/main/kotlin/gg/essential/elementa/components/updateFunc.kt @@ -1,5 +1,9 @@ package gg.essential.elementa.components +import gg.essential.elementa.ElementaVersion +import gg.essential.elementa.UIComponent +import gg.essential.elementa.UIComponent.Flags + /** * Called once at the start of every frame to update any animations and miscellaneous state. * @@ -17,3 +21,23 @@ internal val NOP_UPDATE_FUNC: UpdateFunc = { _, _ -> } internal class NopUpdateFuncList(override val size: Int) : AbstractList() { override fun get(index: Int): UpdateFunc = NOP_UPDATE_FUNC } + +/** + * Internal utility for components which used to use `animationFrame`, and therefore still have to do that for backwards + * compatibility until v8 is enabled, but which then use UpdateFunc once v8 is enabled. + */ +internal fun UIComponent.addUpdateFuncOnV8ReplacingAnimationFrame(func: UpdateFunc) { + // we override animationFrame only for backwards compatibility and use this UpdateFunc on newer versions + ownFlags -= Flags.RequiresAnimationFrame + + addUpdateFunc(object : UpdateFunc { + override fun invoke(dt: Float, dtMs: Int) { + if (Window.of(this@addUpdateFuncOnV8ReplacingAnimationFrame).version < ElementaVersion.v8) { + // handled by animationFrame + removeUpdateFunc(this) + return + } + func(dt, dtMs) + } + }) +} diff --git a/src/main/kotlin/gg/essential/elementa/constraints/AdditiveConstraint.kt b/src/main/kotlin/gg/essential/elementa/constraints/AdditiveConstraint.kt index e290817f..d0f19be3 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/AdditiveConstraint.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/AdditiveConstraint.kt @@ -11,6 +11,8 @@ class AdditiveConstraint( override var recalculate = true override var constrainTo: UIComponent? = null + @Deprecated("See [ElementaVersion.V8].") + @Suppress("DEPRECATION") override fun animationFrame() { super.animationFrame() constraint1.animationFrame() diff --git a/src/main/kotlin/gg/essential/elementa/constraints/CoercionConstraint.kt b/src/main/kotlin/gg/essential/elementa/constraints/CoercionConstraint.kt index 2c686acf..299e22e6 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/CoercionConstraint.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/CoercionConstraint.kt @@ -14,6 +14,8 @@ class CoerceAtMostConstraint( override var recalculate = true override var constrainTo: UIComponent? = null + @Deprecated("See [ElementaVersion.V8].") + @Suppress("DEPRECATION") override fun animationFrame() { super.animationFrame() constraint.animationFrame() @@ -66,6 +68,8 @@ class CoerceAtLeastConstraint( override var recalculate = true override var constrainTo: UIComponent? = null + @Deprecated("See [ElementaVersion.V8].") + @Suppress("DEPRECATION") override fun animationFrame() { super.animationFrame() constraint.animationFrame() @@ -119,6 +123,8 @@ class CoerceInConstraint( override var recalculate = true override var constrainTo: UIComponent? = null + @Deprecated("See [ElementaVersion.V8].") + @Suppress("DEPRECATION") override fun animationFrame() { super.animationFrame() constraint.animationFrame() diff --git a/src/main/kotlin/gg/essential/elementa/constraints/ColorConstraints.kt b/src/main/kotlin/gg/essential/elementa/constraints/ColorConstraints.kt index bbea4035..f394c997 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/ColorConstraints.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/ColorConstraints.kt @@ -1,6 +1,7 @@ package gg.essential.elementa.constraints import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.Window import gg.essential.elementa.constraints.resolution.ConstraintVisitor import gg.essential.elementa.state.BasicState import gg.essential.elementa.state.State @@ -90,22 +91,16 @@ class RainbowColorConstraint(val alpha: Int = 255, val speed: Float = 50f) : Col override var recalculate = true override var constrainTo: UIComponent? = null - private var currentColor: Color = Color.WHITE - private var currentStep = Random.nextInt(500) + private val randomOffset = Random.nextInt(500).toFloat() override fun getColorImpl(component: UIComponent): Color { - return currentColor - } - - override fun animationFrame() { - super.animationFrame() - currentStep++ + val currentStep = randomOffset + (Window.ofOrNull(component)?.animationTimeMs ?: 0) * (244 / 1000f) val red = ((sin((currentStep / speed).toDouble()) + 0.75) * 170).toInt() val green = ((sin(currentStep / speed + 2 * Math.PI / 3) + 0.75) * 170).toInt() val blue = ((sin(currentStep / speed + 4 * Math.PI / 3) + 0.75) * 170).toInt() - currentColor = Color( + return Color( red.coerceIn(0, 255), green.coerceIn(0, 255), blue.coerceIn(0, 255), diff --git a/src/main/kotlin/gg/essential/elementa/constraints/Constraint.kt b/src/main/kotlin/gg/essential/elementa/constraints/Constraint.kt index db402196..fd263565 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/Constraint.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/Constraint.kt @@ -1,6 +1,8 @@ package gg.essential.elementa.constraints +import gg.essential.elementa.ElementaVersion import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.Window import gg.essential.elementa.constraints.animation.AnimationComponent import gg.essential.elementa.constraints.debug.constraintDebugger import gg.essential.elementa.constraints.resolution.ConstraintVisitor @@ -18,6 +20,7 @@ interface SuperConstraint { var recalculate: Boolean var constrainTo: UIComponent? + @Deprecated("See [ElementaVersion.V8].") fun animationFrame() { recalculate = true } @@ -72,37 +75,15 @@ interface PositionConstraint : XConstraint, YConstraint interface XConstraint : SuperConstraint { fun getXPositionImpl(component: UIComponent): Float - fun getXPosition(component: UIComponent): Float { - val debugger = constraintDebugger - if (debugger != null) { - return debugger.evaluate(this, ConstraintType.X, component) - } - - if (recalculate) { - cachedValue = getXPositionImpl(component).roundToRealPixels() - recalculate = false - } - - return cachedValue - } + fun getXPosition(component: UIComponent): Float = + getCachedDebuggable(component, ConstraintType.X) { getXPositionImpl(it).roundToRealPixels() } } interface YConstraint : SuperConstraint { fun getYPositionImpl(component: UIComponent): Float - fun getYPosition(component: UIComponent): Float { - val debugger = constraintDebugger - if (debugger != null) { - return debugger.evaluate(this, ConstraintType.Y, component) - } - - if (recalculate) { - cachedValue = getYPositionImpl(component).roundToRealPixels() - recalculate = false - } - - return cachedValue - } + fun getYPosition(component: UIComponent): Float = + getCachedDebuggable(component, ConstraintType.Y) { getYPositionImpl(it).roundToRealPixels() } } interface SizeConstraint : WidthConstraint, HeightConstraint, RadiusConstraint @@ -110,55 +91,22 @@ interface SizeConstraint : WidthConstraint, HeightConstraint, RadiusConstraint interface RadiusConstraint : SuperConstraint { fun getRadiusImpl(component: UIComponent): Float - fun getRadius(component: UIComponent): Float { - val debugger = constraintDebugger - if (debugger != null) { - return debugger.evaluate(this, ConstraintType.RADIUS, component) - } - - if (recalculate) { - cachedValue = getRadiusImpl(component) - recalculate = false - } - - return cachedValue - } + fun getRadius(component: UIComponent): Float = + getCachedDebuggable(component, ConstraintType.RADIUS) { getRadiusImpl(it).roundToRealPixels() } } interface WidthConstraint : SuperConstraint { fun getWidthImpl(component: UIComponent): Float - fun getWidth(component: UIComponent): Float { - val debugger = constraintDebugger - if (debugger != null) { - return debugger.evaluate(this, ConstraintType.WIDTH, component) - } - - if (recalculate) { - cachedValue = getWidthImpl(component).roundToRealPixels() - recalculate = false - } - - return cachedValue - } + fun getWidth(component: UIComponent): Float = + getCachedDebuggable(component, ConstraintType.WIDTH) { getWidthImpl(it).roundToRealPixels() } } interface HeightConstraint : SuperConstraint { fun getHeightImpl(component: UIComponent): Float - fun getHeight(component: UIComponent): Float { - val debugger = constraintDebugger - if (debugger != null) { - return debugger.evaluate(this, ConstraintType.HEIGHT, component) - } - - if (recalculate) { - cachedValue = getHeightImpl(component).roundToRealPixels() - recalculate = false - } - - return cachedValue - } + fun getHeight(component: UIComponent): Float = + getCachedDebuggable(component, ConstraintType.HEIGHT) { getHeightImpl(it).roundToRealPixels() } fun getTextScale(component: UIComponent): Float { return getHeight(component) @@ -168,14 +116,32 @@ interface HeightConstraint : SuperConstraint { interface ColorConstraint : SuperConstraint { fun getColorImpl(component: UIComponent): Color - fun getColor(component: UIComponent): Color { - if (recalculate) { - cachedValue = getColorImpl(component) - recalculate = false - } + fun getColor(component: UIComponent): Color = + getCached(component) { getColorImpl(it) } +} - return cachedValue +interface MasterConstraint : PositionConstraint, SizeConstraint + +private inline fun SuperConstraint.getCachedDebuggable(component: UIComponent, type: ConstraintType, getImpl: (UIComponent) -> Float): Float { + val debugger = constraintDebugger + if (debugger != null) { + return debugger.evaluate(this, type, component) } + + return getCached(component, getImpl) } -interface MasterConstraint : PositionConstraint, SizeConstraint \ No newline at end of file +internal inline fun SuperConstraint.getCached(component: UIComponent, getImpl: (UIComponent) -> T): T { + if (recalculate) { + cachedValue = getImpl(component) + val window = Window.ofOrNull(component) + if (window != null) { + if (window.version >= ElementaVersion.v8) { + window.cachedConstraints.add(this) + } + recalculate = false + } + } + + return cachedValue +} diff --git a/src/main/kotlin/gg/essential/elementa/constraints/MaxConstraint.kt b/src/main/kotlin/gg/essential/elementa/constraints/MaxConstraint.kt index 51e70b0c..e326cc05 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/MaxConstraint.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/MaxConstraint.kt @@ -9,6 +9,8 @@ class MaxConstraint(val first: SuperConstraint, val second: SuperConstrai override var recalculate = true override var constrainTo: UIComponent? = null + @Deprecated("See [ElementaVersion.V8].") + @Suppress("DEPRECATION") override fun animationFrame() { super.animationFrame() first.animationFrame() diff --git a/src/main/kotlin/gg/essential/elementa/constraints/MinConstraint.kt b/src/main/kotlin/gg/essential/elementa/constraints/MinConstraint.kt index 1b556005..b113a36a 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/MinConstraint.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/MinConstraint.kt @@ -9,6 +9,8 @@ class MinConstraint(val first: SuperConstraint, val second: SuperConstrai override var recalculate = true override var constrainTo: UIComponent? = null + @Deprecated("See [ElementaVersion.V8].") + @Suppress("DEPRECATION") override fun animationFrame() { super.animationFrame() first.animationFrame() diff --git a/src/main/kotlin/gg/essential/elementa/constraints/ScaleConstraint.kt b/src/main/kotlin/gg/essential/elementa/constraints/ScaleConstraint.kt index c6f49718..43e61bd5 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/ScaleConstraint.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/ScaleConstraint.kt @@ -21,6 +21,8 @@ class ScaleConstraint(val constraint: SuperConstraint, value: State Unit) = apply { @@ -160,9 +161,17 @@ class AnimatingConstraints( completeAction = method::run } + internal var updateFunc: UpdateFunc? = null + + @Deprecated("See [ElementaVersion.V8].") override fun animationFrame() { + @Suppress("DEPRECATION") super.animationFrame() + updateCompletion(1) + } + + internal fun updateCompletion(dt: Int) { var anyLeftAnimating = false val x = x @@ -209,7 +218,7 @@ class AnimatingConstraints( if (extraDelayFrames > 0) { anyLeftAnimating = true - extraDelayFrames-- + extraDelayFrames -= dt } if (!anyLeftAnimating) { diff --git a/src/main/kotlin/gg/essential/elementa/constraints/animation/AnimationComponent.kt b/src/main/kotlin/gg/essential/elementa/constraints/animation/AnimationComponent.kt index 7bbdb8cb..0cf0736d 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/animation/AnimationComponent.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/animation/AnimationComponent.kt @@ -1,6 +1,8 @@ package gg.essential.elementa.constraints.animation +import gg.essential.elementa.ElementaVersion import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.Window import gg.essential.elementa.constraints.* import gg.essential.elementa.constraints.debug.constraintDebugger import gg.essential.elementa.constraints.resolution.ConstraintVisitor @@ -18,6 +20,24 @@ sealed class AnimationComponent( var elapsedFrames = 0 var animationPaused = false + private var lastUpdateTime: Long = -1 + + internal fun update(component: UIComponent) { + val window = Window.ofOrNull(component) ?: return + if (window.version < ElementaVersion.v8) return // update handled by `animationFrame` + + val now = window.animationTimeMs + if (lastUpdateTime == -1L) lastUpdateTime = now + val dtMs = (now - lastUpdateTime).toInt() + lastUpdateTime = now + + if (!animationPaused) { + elapsedFrames = (elapsedFrames + dtMs).coerceAtMost(totalFrames + delayFrames) + } + } + + @Deprecated("See [ElementaVersion.V8].") + @Suppress("DEPRECATION") override fun animationFrame() { super.animationFrame() @@ -55,13 +75,16 @@ class XAnimationComponent( override var constrainTo: UIComponent? = null override fun getXPositionImpl(component: UIComponent): Float { + update(component) + val startX = oldConstraint.getXPosition(component) val finalX = newConstraint.getXPosition(component) return startX + ((finalX - startX) * getPercentComplete()) } - // TODO: This is gross, can probably be done in parent! + @Deprecated("See [ElementaVersion.V8].") + @Suppress("DEPRECATION") override fun animationFrame() { super.animationFrame() @@ -91,11 +114,16 @@ class YAnimationComponent( override var constrainTo: UIComponent? = null override fun getYPositionImpl(component: UIComponent): Float { + update(component) + val startX = oldConstraint.getYPosition(component) val finalX = newConstraint.getYPosition(component) return startX + ((finalX - startX) * getPercentComplete()) } + + @Deprecated("See [ElementaVersion.V8].") + @Suppress("DEPRECATION") override fun animationFrame() { super.animationFrame() @@ -125,12 +153,16 @@ class RadiusAnimationComponent( override var constrainTo: UIComponent? = null override fun getRadiusImpl(component: UIComponent): Float { + update(component) + val startX = oldConstraint.getRadius(component) val finalX = newConstraint.getRadius(component) return startX + ((finalX - startX) * getPercentComplete()) } + @Deprecated("See [ElementaVersion.V8].") + @Suppress("DEPRECATION") override fun animationFrame() { super.animationFrame() @@ -160,12 +192,16 @@ class WidthAnimationComponent( override var constrainTo: UIComponent? = null override fun getWidthImpl(component: UIComponent): Float { + update(component) + val startX = oldConstraint.getWidth(component) val finalX = newConstraint.getWidth(component) return startX + ((finalX - startX) * getPercentComplete()) } + @Deprecated("See [ElementaVersion.V8].") + @Suppress("DEPRECATION") override fun animationFrame() { super.animationFrame() @@ -195,6 +231,8 @@ class HeightAnimationComponent( override var constrainTo: UIComponent? = null override fun getHeightImpl(component: UIComponent): Float { + update(component) + val startX = oldConstraint.getHeight(component) val finalX = newConstraint.getHeight(component) @@ -216,6 +254,8 @@ class HeightAnimationComponent( return cachedValue } + @Deprecated("See [ElementaVersion.V8].") + @Suppress("DEPRECATION") override fun animationFrame() { super.animationFrame() @@ -245,6 +285,8 @@ class ColorAnimationComponent( override var constrainTo: UIComponent? = null override fun getColorImpl(component: UIComponent): Color { + update(component) + val startColor = oldConstraint.getColor(component) val endColor = newConstraint.getColor(component) val percentComplete = getPercentComplete() @@ -257,6 +299,8 @@ class ColorAnimationComponent( return Color(newR.roundToInt(), newG.roundToInt(), newB.roundToInt(), newA.roundToInt()) } + @Deprecated("See [ElementaVersion.V8].") + @Suppress("DEPRECATION") override fun animationFrame() { super.animationFrame() @@ -284,6 +328,8 @@ sealed class FieldAnimationComponent( abstract val field: KMutableProperty0<*> + @Deprecated("See [ElementaVersion.V8].") + @Suppress("DEPRECATION") override fun animationFrame() { super.animationFrame() diff --git a/src/main/kotlin/gg/essential/elementa/constraints/debug/NoopConstraintDebugger.kt b/src/main/kotlin/gg/essential/elementa/constraints/debug/NoopConstraintDebugger.kt index 56288c7b..185292da 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/debug/NoopConstraintDebugger.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/debug/NoopConstraintDebugger.kt @@ -3,14 +3,10 @@ package gg.essential.elementa.constraints.debug import gg.essential.elementa.UIComponent import gg.essential.elementa.constraints.ConstraintType import gg.essential.elementa.constraints.SuperConstraint +import gg.essential.elementa.constraints.getCached internal class NoopConstraintDebugger : ConstraintDebugger { override fun evaluate(constraint: SuperConstraint, type: ConstraintType, component: UIComponent): Float { - if (constraint.recalculate) { - constraint.cachedValue = invokeImpl(constraint, type, component) - constraint.recalculate = false - } - - return constraint.cachedValue + return constraint.getCached(component) { invokeImpl(constraint, type, component) } } } diff --git a/src/main/kotlin/gg/essential/elementa/effects/Effect.kt b/src/main/kotlin/gg/essential/elementa/effects/Effect.kt index 05f2fbab..4a4f5223 100644 --- a/src/main/kotlin/gg/essential/elementa/effects/Effect.kt +++ b/src/main/kotlin/gg/essential/elementa/effects/Effect.kt @@ -61,6 +61,7 @@ abstract class Effect { /** * Called in the component's animationFrame function */ + @Deprecated("See [ElementaVersion.V8].") open fun animationFrame() {} /** diff --git a/src/main/kotlin/gg/essential/elementa/effects/RecursiveFadeEffect.kt b/src/main/kotlin/gg/essential/elementa/effects/RecursiveFadeEffect.kt index 58a15795..83b08a02 100644 --- a/src/main/kotlin/gg/essential/elementa/effects/RecursiveFadeEffect.kt +++ b/src/main/kotlin/gg/essential/elementa/effects/RecursiveFadeEffect.kt @@ -72,8 +72,10 @@ class RecursiveFadeEffect constructor( } } + @Deprecated("See [ElementaVersion.V8].") override fun animationFrame() { // We still want the original constraint's colour to recalculate while we are animating + @Suppress("DEPRECATION") originalConstraint.animationFrame() } diff --git a/src/main/kotlin/gg/essential/elementa/markdown/MarkdownComponent.kt b/src/main/kotlin/gg/essential/elementa/markdown/MarkdownComponent.kt index ce446998..4193a417 100644 --- a/src/main/kotlin/gg/essential/elementa/markdown/MarkdownComponent.kt +++ b/src/main/kotlin/gg/essential/elementa/markdown/MarkdownComponent.kt @@ -1,10 +1,13 @@ package gg.essential.elementa.markdown +import gg.essential.elementa.ElementaVersion import gg.essential.elementa.UIComponent import gg.essential.elementa.components.MarkdownNode import gg.essential.elementa.components.TreeListComponent import gg.essential.elementa.components.TreeNode +import gg.essential.elementa.components.UpdateFunc import gg.essential.elementa.components.Window +import gg.essential.elementa.components.addUpdateFuncOnV8ReplacingAnimationFrame import gg.essential.elementa.constraints.HeightConstraint import gg.essential.elementa.dsl.pixels import gg.essential.elementa.events.UIEvent @@ -174,9 +177,16 @@ class MarkdownComponent( } ?: 0f } + init { addUpdateFuncOnV8ReplacingAnimationFrame { _, _ -> update() }} + @Deprecated("See [ElementaVersion.V8].") override fun animationFrame() { + @Suppress("DEPRECATION") super.animationFrame() + if (versionOrV0 >= ElementaVersion.v8) return // handled by UpdateFunc + update() + } + private fun update() { if (needsInitialLayout) { needsInitialLayout = false reparse() @@ -212,7 +222,7 @@ class MarkdownComponent( override fun draw(matrixStack: UMatrixStack) { if (needsInitialLayout) { - animationFrame() + update() } beforeDraw(matrixStack)