Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/Elementa.api
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public abstract class gg/essential/elementa/UIComponent : java/util/Observable,
public fun <init> ()V
public fun addChild (Lgg/essential/elementa/UIComponent;)Lgg/essential/elementa/UIComponent;
public fun addChildren ([Lgg/essential/elementa/UIComponent;)Lgg/essential/elementa/UIComponent;
public final fun addUpdateFunc (Lkotlin/jvm/functions/Function2;)V
public fun afterDraw ()V
public fun afterDraw (Lgg/essential/universal/UMatrixStack;)V
public final fun afterDrawCompat (Lgg/essential/universal/UMatrixStack;)V
Expand Down Expand Up @@ -134,6 +135,7 @@ public abstract class gg/essential/elementa/UIComponent : java/util/Observable,
public fun removeChild (Lgg/essential/elementa/UIComponent;)Lgg/essential/elementa/UIComponent;
public final fun removeEffect (Lgg/essential/elementa/effects/Effect;)V
public final fun removeEffect (Ljava/lang/Class;)V
public final fun removeUpdateFunc (Lkotlin/jvm/functions/Function2;)V
public fun replaceChild (Lgg/essential/elementa/UIComponent;Lgg/essential/elementa/UIComponent;)Lgg/essential/elementa/UIComponent;
protected final fun requireChildrenUnlocked ()V
public final fun setChildOf (Lgg/essential/elementa/UIComponent;)Lgg/essential/elementa/UIComponent;
Expand Down Expand Up @@ -2583,6 +2585,7 @@ public final class gg/essential/elementa/dsl/UtilitiesKt {
public abstract class gg/essential/elementa/effects/Effect {
protected field boundComponent Lgg/essential/elementa/UIComponent;
public fun <init> ()V
protected final fun addUpdateFunc (Lkotlin/jvm/functions/Function2;)V
public fun afterDraw ()V
public fun afterDraw (Lgg/essential/universal/UMatrixStack;)V
public final fun afterDrawCompat (Lgg/essential/universal/UMatrixStack;)V
Expand All @@ -2595,6 +2598,7 @@ public abstract class gg/essential/elementa/effects/Effect {
public final fun beforeDrawCompat (Lgg/essential/universal/UMatrixStack;)V
public final fun bindComponent (Lgg/essential/elementa/UIComponent;)V
protected final fun getBoundComponent ()Lgg/essential/elementa/UIComponent;
protected final fun removeUpdateFunc (Lkotlin/jvm/functions/Function2;)V
protected final fun setBoundComponent (Lgg/essential/elementa/UIComponent;)V
public fun setup ()V
}
Expand Down
264 changes: 263 additions & 1 deletion src/main/kotlin/gg/essential/elementa/UIComponent.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package gg.essential.elementa

import gg.essential.elementa.components.NOP_UPDATE_FUNC
import gg.essential.elementa.components.NopUpdateFuncList
import gg.essential.elementa.components.UIBlock
import gg.essential.elementa.components.UIContainer
import gg.essential.elementa.components.UpdateFunc
import gg.essential.elementa.components.Window
import gg.essential.elementa.constraints.*
import gg.essential.elementa.constraints.animation.*
Expand Down Expand Up @@ -49,14 +52,19 @@ abstract class UIComponent : Observable(), ReferenceHolder {
return field
}
open val children = CopyOnWriteArrayList<UIComponent>().observable()
val effects = mutableListOf<Effect>()
val effects: MutableList<Effect> = mutableListOf<Effect>().observable().apply {
addObserver { _, event ->
updateUpdateFuncsOnChangedEffect(event)
}
}

private var childrenLocked = 0
init {
children.addObserver { _, event ->
requireChildrenUnlocked()
setWindowCacheOnChangedChild(event)
updateFloatingComponentsOnChangedChild(event)
updateUpdateFuncsOnChangedChild(event)
}
}

Expand Down Expand Up @@ -1050,6 +1058,258 @@ abstract class UIComponent : Observable(), ReferenceHolder {
}
}

//region Public UpdateFunc API
fun addUpdateFunc(func: UpdateFunc) {
val updateFuncs = updateFuncs ?: mutableListOf<UpdateFunc>().also { updateFuncs = it }
val index = updateFuncs.size
updateFuncs.add(func)

val indexInWindow = allocUpdateFuncs(index, 1)
if (indexInWindow != -1) {
cachedWindow!!.allUpdateFuncs[indexInWindow] = func
assertUpdateFuncInvariants()
}
}

fun removeUpdateFunc(func: UpdateFunc) {
val updateFuncs = updateFuncs ?: return
val index = updateFuncs.indexOf(func)
if (index == -1) return
updateFuncs.removeAt(index)

freeUpdateFuncs(index, 1)
}
//endregion

//region Internal UpdateFunc tracking
private var updateFuncParent: UIComponent? = null
private var updateFuncs: MutableList<UpdateFunc>? = null // only allocated if used
private var effectUpdateFuncs = 0 // count of effect funcs
private var totalUpdateFuncs = 0 // count of own funcs + effect funcs + children total funcs

private fun localUpdateFuncIndexForEffect(effectIndex: Int, indexInEffect: Int): Int {
var localIndex = updateFuncs?.size ?: 0
for ((otherEffectIndex, otherEffect) in effects.withIndex()) {
if (otherEffectIndex >= effectIndex) {
break
} else {
if (otherEffect.updateFuncParent != this) continue // can happen if added to two components at the same time
localIndex += otherEffect.updateFuncs?.size ?: 0
}
}
localIndex += indexInEffect
return localIndex
}

private fun localUpdateFuncIndexForChild(childIndex: Int, indexInChild: Int): Int {
var localIndex = (updateFuncs?.size ?: 0) + effectUpdateFuncs
for ((otherChildIndex, otherChild) in children.withIndex()) {
if (otherChildIndex >= childIndex) {
break
} else {
if (otherChild.updateFuncParent != this) continue // can happen if added to two components at the same time
localIndex += otherChild.totalUpdateFuncs
}
}
localIndex += indexInChild
return localIndex
}

internal fun addUpdateFunc(effect: Effect, indexInEffect: Int, func: UpdateFunc) {
effectUpdateFuncs++
val indexInWindow = allocUpdateFuncs(localUpdateFuncIndexForEffect(effects.indexOf(effect), indexInEffect), 1)
if (indexInWindow != -1) {
cachedWindow!!.allUpdateFuncs[indexInWindow] = func
assertUpdateFuncInvariants()
}
}

internal fun removeUpdateFunc(effect: Effect, indexInEffect: Int) {
effectUpdateFuncs--
freeUpdateFuncs(localUpdateFuncIndexForEffect(effects.indexOf(effect), indexInEffect), 1)
}

private fun allocUpdateFuncs(childIndex: Int, indexInChild: Int, count: Int): Int {
return allocUpdateFuncs(localUpdateFuncIndexForChild(childIndex, indexInChild), count)
}

private fun freeUpdateFuncs(childIndex: Int, indexInChild: Int, count: Int) {
freeUpdateFuncs(localUpdateFuncIndexForChild(childIndex, indexInChild), count)
}

private fun allocUpdateFuncs(localIndex: Int, count: Int): Int {
totalUpdateFuncs += count
if (this is Window) {
if (nextUpdateFuncIndex > localIndex) {
nextUpdateFuncIndex += count
}
if (count == 1) {
allUpdateFuncs.add(localIndex, NOP_UPDATE_FUNC)
} else {
allUpdateFuncs.addAll(localIndex, NopUpdateFuncList(count))
}
return localIndex
} else {
val parent = updateFuncParent ?: return -1
return parent.allocUpdateFuncs(parent.children.indexOf(this), localIndex, count)
}
}

private fun freeUpdateFuncs(localIndex: Int, count: Int) {
totalUpdateFuncs -= count
if (this is Window) {
if (nextUpdateFuncIndex > localIndex) {
nextUpdateFuncIndex -= min(count, nextUpdateFuncIndex - localIndex)
}
if (count == 1) {
allUpdateFuncs.removeAt(localIndex)
} else {
allUpdateFuncs.subList(localIndex, localIndex + count).clear()
}
assertUpdateFuncInvariants()
} else {
val parent = updateFuncParent ?: return
parent.freeUpdateFuncs(parent.children.indexOf(this), localIndex, count)
}
}

private fun updateUpdateFuncsOnChangedChild(possibleEvent: Any) {
@Suppress("UNCHECKED_CAST")
when (val event = possibleEvent as? ObservableListEvent<UIComponent> ?: return) {
is ObservableAddEvent -> {
val (childIndex, child) = event.element
child.updateFuncParent?.let { oldParent ->
oldParent.updateUpdateFuncsOnChangedChild(ObservableRemoveEvent(
IndexedValue(oldParent.children.indexOf(child), child)))
}
assert(child.updateFuncParent == null)
child.updateFuncParent = this

if (child.totalUpdateFuncs == 0) return
var indexInWindow = allocUpdateFuncs(childIndex, 0, child.totalUpdateFuncs)
if (indexInWindow == -1) return
val allUpdateFuncs = cachedWindow!!.allUpdateFuncs
fun register(component: UIComponent) {
component.updateFuncs?.let { funcs ->
for (func in funcs) {
allUpdateFuncs[indexInWindow++] = func
}
}
component.effects.forEach { effect ->
if (effect.updateFuncParent != component) return@forEach // can happen if added to two components at the same time
effect.updateFuncs?.let { funcs ->
for (func in funcs) {
allUpdateFuncs[indexInWindow++] = func
}
}
}
component.children.forEach { child ->
if (child.updateFuncParent != component) return@forEach // can happen if added to two components at the same time
register(child)
}
}
register(child)
assertUpdateFuncInvariants()
}
is ObservableRemoveEvent -> {
val (childIndex, child) = event.element
if (child.updateFuncParent != this) return // double remove can happen if added to two component at once
child.updateFuncParent = null

if (child.totalUpdateFuncs == 0) return
freeUpdateFuncs(childIndex, 0, child.totalUpdateFuncs)
}
is ObservableClearEvent -> {
event.oldChildren.forEach { if (it.updateFuncParent == this) it.updateFuncParent = null }

val remainingFuncs = (updateFuncs?.size ?: 0) + effectUpdateFuncs
val removedFuncs = totalUpdateFuncs - remainingFuncs
freeUpdateFuncs(remainingFuncs, removedFuncs)
}
}
}

private fun updateUpdateFuncsOnChangedEffect(possibleEvent: Any) {
@Suppress("UNCHECKED_CAST")
when (val event = possibleEvent as? ObservableListEvent<Effect> ?: return) {
is ObservableAddEvent -> {
val (effectIndex, effect) = event.element
effect.updateFuncParent?.let { oldParent ->
oldParent.updateUpdateFuncsOnChangedEffect(ObservableRemoveEvent(
IndexedValue(oldParent.effects.indexOf(effect), effect)))
}
assert(effect.updateFuncParent == null)
effect.updateFuncParent = this

val funcs = effect.updateFuncs ?: return
if (funcs.isEmpty()) return
effectUpdateFuncs += funcs.size
var indexInWindow = allocUpdateFuncs(localUpdateFuncIndexForEffect(effectIndex, 0), funcs.size)
if (indexInWindow == -1) return
val allUpdateFuncs = cachedWindow!!.allUpdateFuncs
for (func in funcs) {
allUpdateFuncs[indexInWindow++] = func
}
assertUpdateFuncInvariants()
}
is ObservableRemoveEvent -> {
val (effectIndex, effect) = event.element
if (effect.updateFuncParent != this) return // double remove can happen if added to two component at once
effect.updateFuncParent = null

val funcs = effect.updateFuncs?.size ?: 0
if (funcs == 0) return
effectUpdateFuncs -= funcs
freeUpdateFuncs(localUpdateFuncIndexForEffect(effectIndex, 0), funcs)
}
is ObservableClearEvent -> {
event.oldChildren.forEach { if (it.updateFuncParent == this) it.updateFuncParent = null }

val removedFuncs = effectUpdateFuncs
effectUpdateFuncs = 0
freeUpdateFuncs(updateFuncs?.size ?: 0, removedFuncs)
}
}
}

internal fun assertUpdateFuncInvariants() {
if (!ASSERT_UPDATE_FUNC_INVARINTS) return

val window = cachedWindow ?: return
val allUpdateFuncs = window.allUpdateFuncs

var indexInWindow = 0

fun visit(component: UIComponent) {
val effectUpdateFuncs = component.effects.sumOf { if (it.updateFuncParent == component) it.updateFuncs?.size ?: 0 else 0 }
val childUpdateFuncs = component.children.sumOf { if (it.updateFuncParent == component) it.totalUpdateFuncs else 0 }
assert(component.effectUpdateFuncs == effectUpdateFuncs)
assert(component.totalUpdateFuncs == (component.updateFuncs?.size ?: 0) + effectUpdateFuncs + childUpdateFuncs)

component.updateFuncs?.let { funcs ->
for (func in funcs) {
assert(func == allUpdateFuncs[indexInWindow++])
}
}
component.effects.forEach { effect ->
if (effect.updateFuncParent != component) return@forEach // can happen if added to two components at the same time
effect.updateFuncs?.let { funcs ->
for (func in funcs) {
assert(func == allUpdateFuncs[indexInWindow++])
}
}
}
component.children.forEach { child ->
if (child.updateFuncParent != component) return@forEach // can happen if added to two components at the same time
visit(child)
}
}
visit(window)

assert(indexInWindow == allUpdateFuncs.size)
}
//endregion

/**
* Field animation API
*/
Expand Down Expand Up @@ -1264,6 +1524,8 @@ abstract class UIComponent : Observable(), ReferenceHolder {
// Default value for componentName used as marker for lazy init.
private val defaultComponentName = String()

private val ASSERT_UPDATE_FUNC_INVARINTS = System.getProperty("elementa.debug.assertUpdateFuncInvariants").toBoolean()

val DEBUG_OUTLINE_WIDTH = System.getProperty("elementa.debug.width")?.toDoubleOrNull() ?: 2.0

/**
Expand Down
21 changes: 21 additions & 0 deletions src/main/kotlin/gg/essential/elementa/components/Window.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ class Window @JvmOverloads constructor(
val animationFPS: Int = 244
) : UIComponent() {
private var systemTime = -1L

private var lastDrawTime: Long = -1

internal var allUpdateFuncs: MutableList<UpdateFunc> = mutableListOf()
internal var nextUpdateFuncIndex = 0

private var currentMouseButton = -1

private var legacyFloatingComponents = mutableListOf<UIComponent>()
Expand All @@ -43,6 +49,7 @@ class Window @JvmOverloads constructor(

init {
super.parent = this
cachedWindow = this
}

override fun afterInitialization() {
Expand Down Expand Up @@ -74,9 +81,23 @@ class Window @JvmOverloads constructor(

if (systemTime == -1L)
systemTime = System.currentTimeMillis()
if (lastDrawTime == -1L)
lastDrawTime = System.currentTimeMillis()

val now = System.currentTimeMillis()
val dtMs = now - lastDrawTime
lastDrawTime = now

try {

assertUpdateFuncInvariants()
nextUpdateFuncIndex = 0
while (true) {
val func = allUpdateFuncs.getOrNull(nextUpdateFuncIndex) ?: break
nextUpdateFuncIndex++
func(dtMs / 1000f, dtMs.toInt())
}

//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
Expand Down
19 changes: 19 additions & 0 deletions src/main/kotlin/gg/essential/elementa/components/updateFunc.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package gg.essential.elementa.components

/**
* Called once at the start of every frame to update any animations and miscellaneous state.
*
* @param dt Time (in seconds) since last frame
* @param dtMs Time (in milliseconds) since last frame
*
* This differs from `(dt / 1000).toInt()` in that it will account for the fractional milliseconds which would
* otherwise be lost to rounding. E.g. if there are three frames each lasting 16.4ms,
* `(dt / 1000).toInt()` would be 16 each time, but `dtMs` will be 16 on the first two frames and 17 on the third.
*/
typealias UpdateFunc = (dt: Float, dtMs: Int) -> Unit

internal val NOP_UPDATE_FUNC: UpdateFunc = { _, _ -> }

internal class NopUpdateFuncList(override val size: Int) : AbstractList<UpdateFunc>() {
override fun get(index: Int): UpdateFunc = NOP_UPDATE_FUNC
}
Loading