|
1 | 1 | package gg.essential.elementa |
2 | 2 |
|
| 3 | +import gg.essential.elementa.components.NOP_UPDATE_FUNC |
| 4 | +import gg.essential.elementa.components.NopUpdateFuncList |
3 | 5 | import gg.essential.elementa.components.UIBlock |
4 | 6 | import gg.essential.elementa.components.UIContainer |
| 7 | +import gg.essential.elementa.components.UpdateFunc |
5 | 8 | import gg.essential.elementa.components.Window |
6 | 9 | import gg.essential.elementa.constraints.* |
7 | 10 | import gg.essential.elementa.constraints.animation.* |
@@ -49,14 +52,19 @@ abstract class UIComponent : Observable(), ReferenceHolder { |
49 | 52 | return field |
50 | 53 | } |
51 | 54 | open val children = CopyOnWriteArrayList<UIComponent>().observable() |
52 | | - val effects = mutableListOf<Effect>() |
| 55 | + val effects: MutableList<Effect> = mutableListOf<Effect>().observable().apply { |
| 56 | + addObserver { _, event -> |
| 57 | + updateUpdateFuncsOnChangedEffect(event) |
| 58 | + } |
| 59 | + } |
53 | 60 |
|
54 | 61 | private var childrenLocked = 0 |
55 | 62 | init { |
56 | 63 | children.addObserver { _, event -> |
57 | 64 | requireChildrenUnlocked() |
58 | 65 | setWindowCacheOnChangedChild(event) |
59 | 66 | updateFloatingComponentsOnChangedChild(event) |
| 67 | + updateUpdateFuncsOnChangedChild(event) |
60 | 68 | } |
61 | 69 | } |
62 | 70 |
|
@@ -1050,6 +1058,258 @@ abstract class UIComponent : Observable(), ReferenceHolder { |
1050 | 1058 | } |
1051 | 1059 | } |
1052 | 1060 |
|
| 1061 | + //region Public UpdateFunc API |
| 1062 | + fun addUpdateFunc(func: UpdateFunc) { |
| 1063 | + val updateFuncs = updateFuncs ?: mutableListOf<UpdateFunc>().also { updateFuncs = it } |
| 1064 | + val index = updateFuncs.size |
| 1065 | + updateFuncs.add(func) |
| 1066 | + |
| 1067 | + val indexInWindow = allocUpdateFuncs(index, 1) |
| 1068 | + if (indexInWindow != -1) { |
| 1069 | + cachedWindow!!.allUpdateFuncs[indexInWindow] = func |
| 1070 | + assertUpdateFuncInvariants() |
| 1071 | + } |
| 1072 | + } |
| 1073 | + |
| 1074 | + fun removeUpdateFunc(func: UpdateFunc) { |
| 1075 | + val updateFuncs = updateFuncs ?: return |
| 1076 | + val index = updateFuncs.indexOf(func) |
| 1077 | + if (index == -1) return |
| 1078 | + updateFuncs.removeAt(index) |
| 1079 | + |
| 1080 | + freeUpdateFuncs(index, 1) |
| 1081 | + } |
| 1082 | + //endregion |
| 1083 | + |
| 1084 | + //region Internal UpdateFunc tracking |
| 1085 | + private var updateFuncParent: UIComponent? = null |
| 1086 | + private var updateFuncs: MutableList<UpdateFunc>? = null // only allocated if used |
| 1087 | + private var effectUpdateFuncs = 0 // count of effect funcs |
| 1088 | + private var totalUpdateFuncs = 0 // count of own funcs + effect funcs + children total funcs |
| 1089 | + |
| 1090 | + private fun localUpdateFuncIndexForEffect(effectIndex: Int, indexInEffect: Int): Int { |
| 1091 | + var localIndex = updateFuncs?.size ?: 0 |
| 1092 | + for ((otherEffectIndex, otherEffect) in effects.withIndex()) { |
| 1093 | + if (otherEffectIndex >= effectIndex) { |
| 1094 | + break |
| 1095 | + } else { |
| 1096 | + if (otherEffect.updateFuncParent != this) continue // can happen if added to two components at the same time |
| 1097 | + localIndex += otherEffect.updateFuncs?.size ?: 0 |
| 1098 | + } |
| 1099 | + } |
| 1100 | + localIndex += indexInEffect |
| 1101 | + return localIndex |
| 1102 | + } |
| 1103 | + |
| 1104 | + private fun localUpdateFuncIndexForChild(childIndex: Int, indexInChild: Int): Int { |
| 1105 | + var localIndex = (updateFuncs?.size ?: 0) + effectUpdateFuncs |
| 1106 | + for ((otherChildIndex, otherChild) in children.withIndex()) { |
| 1107 | + if (otherChildIndex >= childIndex) { |
| 1108 | + break |
| 1109 | + } else { |
| 1110 | + if (otherChild.updateFuncParent != this) continue // can happen if added to two components at the same time |
| 1111 | + localIndex += otherChild.totalUpdateFuncs |
| 1112 | + } |
| 1113 | + } |
| 1114 | + localIndex += indexInChild |
| 1115 | + return localIndex |
| 1116 | + } |
| 1117 | + |
| 1118 | + internal fun addUpdateFunc(effect: Effect, indexInEffect: Int, func: UpdateFunc) { |
| 1119 | + effectUpdateFuncs++ |
| 1120 | + val indexInWindow = allocUpdateFuncs(localUpdateFuncIndexForEffect(effects.indexOf(effect), indexInEffect), 1) |
| 1121 | + if (indexInWindow != -1) { |
| 1122 | + cachedWindow!!.allUpdateFuncs[indexInWindow] = func |
| 1123 | + assertUpdateFuncInvariants() |
| 1124 | + } |
| 1125 | + } |
| 1126 | + |
| 1127 | + internal fun removeUpdateFunc(effect: Effect, indexInEffect: Int) { |
| 1128 | + effectUpdateFuncs-- |
| 1129 | + freeUpdateFuncs(localUpdateFuncIndexForEffect(effects.indexOf(effect), indexInEffect), 1) |
| 1130 | + } |
| 1131 | + |
| 1132 | + private fun allocUpdateFuncs(childIndex: Int, indexInChild: Int, count: Int): Int { |
| 1133 | + return allocUpdateFuncs(localUpdateFuncIndexForChild(childIndex, indexInChild), count) |
| 1134 | + } |
| 1135 | + |
| 1136 | + private fun freeUpdateFuncs(childIndex: Int, indexInChild: Int, count: Int) { |
| 1137 | + freeUpdateFuncs(localUpdateFuncIndexForChild(childIndex, indexInChild), count) |
| 1138 | + } |
| 1139 | + |
| 1140 | + private fun allocUpdateFuncs(localIndex: Int, count: Int): Int { |
| 1141 | + totalUpdateFuncs += count |
| 1142 | + if (this is Window) { |
| 1143 | + if (nextUpdateFuncIndex > localIndex) { |
| 1144 | + nextUpdateFuncIndex += count |
| 1145 | + } |
| 1146 | + if (count == 1) { |
| 1147 | + allUpdateFuncs.add(localIndex, NOP_UPDATE_FUNC) |
| 1148 | + } else { |
| 1149 | + allUpdateFuncs.addAll(localIndex, NopUpdateFuncList(count)) |
| 1150 | + } |
| 1151 | + return localIndex |
| 1152 | + } else { |
| 1153 | + val parent = updateFuncParent ?: return -1 |
| 1154 | + return parent.allocUpdateFuncs(parent.children.indexOf(this), localIndex, count) |
| 1155 | + } |
| 1156 | + } |
| 1157 | + |
| 1158 | + private fun freeUpdateFuncs(localIndex: Int, count: Int) { |
| 1159 | + totalUpdateFuncs -= count |
| 1160 | + if (this is Window) { |
| 1161 | + if (nextUpdateFuncIndex > localIndex) { |
| 1162 | + nextUpdateFuncIndex -= min(count, nextUpdateFuncIndex - localIndex) |
| 1163 | + } |
| 1164 | + if (count == 1) { |
| 1165 | + allUpdateFuncs.removeAt(localIndex) |
| 1166 | + } else { |
| 1167 | + allUpdateFuncs.subList(localIndex, localIndex + count).clear() |
| 1168 | + } |
| 1169 | + assertUpdateFuncInvariants() |
| 1170 | + } else { |
| 1171 | + val parent = updateFuncParent ?: return |
| 1172 | + parent.freeUpdateFuncs(parent.children.indexOf(this), localIndex, count) |
| 1173 | + } |
| 1174 | + } |
| 1175 | + |
| 1176 | + private fun updateUpdateFuncsOnChangedChild(possibleEvent: Any) { |
| 1177 | + @Suppress("UNCHECKED_CAST") |
| 1178 | + when (val event = possibleEvent as? ObservableListEvent<UIComponent> ?: return) { |
| 1179 | + is ObservableAddEvent -> { |
| 1180 | + val (childIndex, child) = event.element |
| 1181 | + child.updateFuncParent?.let { oldParent -> |
| 1182 | + oldParent.updateUpdateFuncsOnChangedChild(ObservableRemoveEvent( |
| 1183 | + IndexedValue(oldParent.children.indexOf(child), child))) |
| 1184 | + } |
| 1185 | + assert(child.updateFuncParent == null) |
| 1186 | + child.updateFuncParent = this |
| 1187 | + |
| 1188 | + if (child.totalUpdateFuncs == 0) return |
| 1189 | + var indexInWindow = allocUpdateFuncs(childIndex, 0, child.totalUpdateFuncs) |
| 1190 | + if (indexInWindow == -1) return |
| 1191 | + val allUpdateFuncs = cachedWindow!!.allUpdateFuncs |
| 1192 | + fun register(component: UIComponent) { |
| 1193 | + component.updateFuncs?.let { funcs -> |
| 1194 | + for (func in funcs) { |
| 1195 | + allUpdateFuncs[indexInWindow++] = func |
| 1196 | + } |
| 1197 | + } |
| 1198 | + component.effects.forEach { effect -> |
| 1199 | + if (effect.updateFuncParent != component) return@forEach // can happen if added to two components at the same time |
| 1200 | + effect.updateFuncs?.let { funcs -> |
| 1201 | + for (func in funcs) { |
| 1202 | + allUpdateFuncs[indexInWindow++] = func |
| 1203 | + } |
| 1204 | + } |
| 1205 | + } |
| 1206 | + component.children.forEach { child -> |
| 1207 | + if (child.updateFuncParent != component) return@forEach // can happen if added to two components at the same time |
| 1208 | + register(child) |
| 1209 | + } |
| 1210 | + } |
| 1211 | + register(child) |
| 1212 | + assertUpdateFuncInvariants() |
| 1213 | + } |
| 1214 | + is ObservableRemoveEvent -> { |
| 1215 | + val (childIndex, child) = event.element |
| 1216 | + if (child.updateFuncParent != this) return // double remove can happen if added to two component at once |
| 1217 | + child.updateFuncParent = null |
| 1218 | + |
| 1219 | + if (child.totalUpdateFuncs == 0) return |
| 1220 | + freeUpdateFuncs(childIndex, 0, child.totalUpdateFuncs) |
| 1221 | + } |
| 1222 | + is ObservableClearEvent -> { |
| 1223 | + event.oldChildren.forEach { if (it.updateFuncParent == this) it.updateFuncParent = null } |
| 1224 | + |
| 1225 | + val remainingFuncs = (updateFuncs?.size ?: 0) + effectUpdateFuncs |
| 1226 | + val removedFuncs = totalUpdateFuncs - remainingFuncs |
| 1227 | + freeUpdateFuncs(remainingFuncs, removedFuncs) |
| 1228 | + } |
| 1229 | + } |
| 1230 | + } |
| 1231 | + |
| 1232 | + private fun updateUpdateFuncsOnChangedEffect(possibleEvent: Any) { |
| 1233 | + @Suppress("UNCHECKED_CAST") |
| 1234 | + when (val event = possibleEvent as? ObservableListEvent<Effect> ?: return) { |
| 1235 | + is ObservableAddEvent -> { |
| 1236 | + val (effectIndex, effect) = event.element |
| 1237 | + effect.updateFuncParent?.let { oldParent -> |
| 1238 | + oldParent.updateUpdateFuncsOnChangedEffect(ObservableRemoveEvent( |
| 1239 | + IndexedValue(oldParent.effects.indexOf(effect), effect))) |
| 1240 | + } |
| 1241 | + assert(effect.updateFuncParent == null) |
| 1242 | + effect.updateFuncParent = this |
| 1243 | + |
| 1244 | + val funcs = effect.updateFuncs ?: return |
| 1245 | + if (funcs.isEmpty()) return |
| 1246 | + effectUpdateFuncs += funcs.size |
| 1247 | + var indexInWindow = allocUpdateFuncs(localUpdateFuncIndexForEffect(effectIndex, 0), funcs.size) |
| 1248 | + if (indexInWindow == -1) return |
| 1249 | + val allUpdateFuncs = cachedWindow!!.allUpdateFuncs |
| 1250 | + for (func in funcs) { |
| 1251 | + allUpdateFuncs[indexInWindow++] = func |
| 1252 | + } |
| 1253 | + assertUpdateFuncInvariants() |
| 1254 | + } |
| 1255 | + is ObservableRemoveEvent -> { |
| 1256 | + val (effectIndex, effect) = event.element |
| 1257 | + if (effect.updateFuncParent != this) return // double remove can happen if added to two component at once |
| 1258 | + effect.updateFuncParent = null |
| 1259 | + |
| 1260 | + val funcs = effect.updateFuncs?.size ?: 0 |
| 1261 | + if (funcs == 0) return |
| 1262 | + effectUpdateFuncs -= funcs |
| 1263 | + freeUpdateFuncs(localUpdateFuncIndexForEffect(effectIndex, 0), funcs) |
| 1264 | + } |
| 1265 | + is ObservableClearEvent -> { |
| 1266 | + event.oldChildren.forEach { if (it.updateFuncParent == this) it.updateFuncParent = null } |
| 1267 | + |
| 1268 | + val removedFuncs = effectUpdateFuncs |
| 1269 | + effectUpdateFuncs = 0 |
| 1270 | + freeUpdateFuncs(updateFuncs?.size ?: 0, removedFuncs) |
| 1271 | + } |
| 1272 | + } |
| 1273 | + } |
| 1274 | + |
| 1275 | + internal fun assertUpdateFuncInvariants() { |
| 1276 | + if (!ASSERT_UPDATE_FUNC_INVARINTS) return |
| 1277 | + |
| 1278 | + val window = cachedWindow ?: return |
| 1279 | + val allUpdateFuncs = window.allUpdateFuncs |
| 1280 | + |
| 1281 | + var indexInWindow = 0 |
| 1282 | + |
| 1283 | + fun visit(component: UIComponent) { |
| 1284 | + val effectUpdateFuncs = component.effects.sumOf { if (it.updateFuncParent == component) it.updateFuncs?.size ?: 0 else 0 } |
| 1285 | + val childUpdateFuncs = component.children.sumOf { if (it.updateFuncParent == component) it.totalUpdateFuncs else 0 } |
| 1286 | + assert(component.effectUpdateFuncs == effectUpdateFuncs) |
| 1287 | + assert(component.totalUpdateFuncs == (component.updateFuncs?.size ?: 0) + effectUpdateFuncs + childUpdateFuncs) |
| 1288 | + |
| 1289 | + component.updateFuncs?.let { funcs -> |
| 1290 | + for (func in funcs) { |
| 1291 | + assert(func == allUpdateFuncs[indexInWindow++]) |
| 1292 | + } |
| 1293 | + } |
| 1294 | + component.effects.forEach { effect -> |
| 1295 | + if (effect.updateFuncParent != component) return@forEach // can happen if added to two components at the same time |
| 1296 | + effect.updateFuncs?.let { funcs -> |
| 1297 | + for (func in funcs) { |
| 1298 | + assert(func == allUpdateFuncs[indexInWindow++]) |
| 1299 | + } |
| 1300 | + } |
| 1301 | + } |
| 1302 | + component.children.forEach { child -> |
| 1303 | + if (child.updateFuncParent != component) return@forEach // can happen if added to two components at the same time |
| 1304 | + visit(child) |
| 1305 | + } |
| 1306 | + } |
| 1307 | + visit(window) |
| 1308 | + |
| 1309 | + assert(indexInWindow == allUpdateFuncs.size) |
| 1310 | + } |
| 1311 | + //endregion |
| 1312 | + |
1053 | 1313 | /** |
1054 | 1314 | * Field animation API |
1055 | 1315 | */ |
@@ -1264,6 +1524,8 @@ abstract class UIComponent : Observable(), ReferenceHolder { |
1264 | 1524 | // Default value for componentName used as marker for lazy init. |
1265 | 1525 | private val defaultComponentName = String() |
1266 | 1526 |
|
| 1527 | + private val ASSERT_UPDATE_FUNC_INVARINTS = System.getProperty("elementa.debug.assertUpdateFuncInvariants").toBoolean() |
| 1528 | + |
1267 | 1529 | val DEBUG_OUTLINE_WIDTH = System.getProperty("elementa.debug.width")?.toDoubleOrNull() ?: 2.0 |
1268 | 1530 |
|
1269 | 1531 | /** |
|
0 commit comments