Skip to content

Commit c199a84

Browse files
authored
UIComponent: Optimize mouseMove and dragMouse methods
Previously these would both just traverse the entire tree and evaluate the constraints of all components. In Essential, only components which can be dragged care about drag events, so most components don't, and since we primarily use the `hoveredState` from unstable Elementa, virtually no components need the mouseEnter/Leave listeners (which is what mouseMove is for). This commit thusly optimizes the two methods to only traverse the narrow subtree which has components that might be interested. For simplicity, this is a best-effort optimization (e.g. it may still visit components which have had such listeners at some point but no longer do now); it should however be completely correct in that it will behave identical to how it behaved prior to this commit, just consume less CPU where easily possible. Only exception are the `lastDraggedMouseX/Y` properties which are impossible to accurately emulate without traversing every component. They should really have been private and only in Window to begin with... This commit compromises by accepting that their behavior may be different now if set manually or if dragMouse is called manually, but still preserves the rough meaning if it's merely read from in e.g. `mouseRelease`. The exact behavior of these properties was sufficiently unexpected that hopefully no one will have tried to rely on their exact behavior to begin with. Note that the Flags class and tracking introduced in this commit also support `Effect`s, despite neither of the two methods being available in `Effect`s. This is because we'll also use the same mechanism with `animationFrame` in the future. GitHub: #156
1 parent 2d61e23 commit c199a84

File tree

3 files changed

+175
-20
lines changed

3 files changed

+175
-20
lines changed

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

Lines changed: 140 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import gg.essential.universal.UResolution
2626
import org.lwjgl.opengl.GL11
2727
import java.awt.Color
2828
import java.util.*
29+
import java.util.concurrent.ConcurrentHashMap
2930
import java.util.concurrent.ConcurrentLinkedDeque
3031
import java.util.concurrent.CopyOnWriteArrayList
3132
import java.util.function.BiConsumer
@@ -56,6 +57,7 @@ abstract class UIComponent : Observable(), ReferenceHolder {
5657
val effects: MutableList<Effect> = mutableListOf<Effect>().observable().apply {
5758
addObserver { _, event ->
5859
updateUpdateFuncsOnChangedEffect(event)
60+
updateEffectFlagsOnChangedEffect(event)
5961
}
6062
}
6163

@@ -66,6 +68,7 @@ abstract class UIComponent : Observable(), ReferenceHolder {
6668
setWindowCacheOnChangedChild(event)
6769
updateFloatingComponentsOnChangedChild(event)
6870
updateUpdateFuncsOnChangedChild(event)
71+
updateCombinedFlagsOnChangedChild(event)
6972
}
7073
}
7174

@@ -81,8 +84,14 @@ abstract class UIComponent : Observable(), ReferenceHolder {
8184
notifyObservers(constraints)
8285
}
8386

84-
var lastDraggedMouseX: Double? = null
85-
var lastDraggedMouseY: Double? = null
87+
@Deprecated("This property should have been private and probably does not do what you expect it to.")
88+
var lastDraggedMouseX: Double?
89+
get() = Window.ofOrNull(this)?.prevDraggedMouseX?.toDouble()
90+
set(_) {}
91+
@Deprecated("This property should have been private and probably does not do what you expect it to.")
92+
var lastDraggedMouseY: Double?
93+
get() = Window.ofOrNull(this)?.prevDraggedMouseY?.toDouble()
94+
set(_) {}
8695

8796
/* Bubbling Events */
8897
var mouseScrollListeners = mutableListOf<UIComponent.(UIScrollEvent) -> Unit>()
@@ -93,8 +102,11 @@ abstract class UIComponent : Observable(), ReferenceHolder {
93102
/* Non-Bubbling Events */
94103
val mouseReleaseListeners = mutableListOf<UIComponent.() -> Unit>()
95104
val mouseEnterListeners = mutableListOf<UIComponent.() -> Unit>()
105+
get() = field.also { ownFlags += Flags.RequiresMouseMove }
96106
val mouseLeaveListeners = mutableListOf<UIComponent.() -> Unit>()
107+
get() = field.also { ownFlags += Flags.RequiresMouseMove }
97108
val mouseDragListeners = mutableListOf<UIComponent.(mouseX: Float, mouseY: Float, button: Int) -> Unit>()
109+
get() = field.also { ownFlags += Flags.RequiresMouseDrag }
98110
val keyTypedListeners = mutableListOf<UIComponent.(typedChar: Char, keyCode: Int) -> Unit>()
99111

100112
private var currentlyHovered = false
@@ -143,6 +155,73 @@ abstract class UIComponent : Observable(), ReferenceHolder {
143155
children.forEach { it.recursivelySetWindowCache(window) }
144156
}
145157

158+
//region Internal flags tracking
159+
/** Flags which apply to this component specifically. */
160+
internal var ownFlags = Flags.initialFor(javaClass)
161+
set(newValue) {
162+
val oldValue = field
163+
if (oldValue == newValue) return
164+
field = newValue
165+
if (oldValue in newValue) { // merely additions?
166+
combinedFlags += newValue
167+
} else {
168+
recomputeCombinedFlags()
169+
}
170+
}
171+
/** Flags which apply to one of the effects of tis component. */
172+
internal var effectFlags = Flags(0u)
173+
set(newValue) {
174+
val oldValue = field
175+
if (oldValue == newValue) return
176+
field = newValue
177+
if (oldValue in newValue) { // merely additions?
178+
combinedFlags += newValue
179+
} else {
180+
recomputeCombinedFlags()
181+
}
182+
}
183+
/** Combined flags of this component, its effects, and its children. */
184+
internal var combinedFlags = ownFlags
185+
set(newValue) {
186+
val oldValue = field
187+
if (oldValue == newValue) return
188+
field = newValue
189+
if (hasParent && parent != this) {
190+
if (oldValue in newValue) { // merely additions?
191+
parent.combinedFlags += newValue
192+
} else {
193+
parent.recomputeCombinedFlags()
194+
}
195+
}
196+
}
197+
198+
internal fun recomputeEffectFlags() {
199+
effectFlags = effects.fold(Flags(0u)) { acc, effect -> acc + effect.flags }
200+
}
201+
202+
private fun recomputeCombinedFlags() {
203+
combinedFlags = children.fold(ownFlags + effectFlags) { acc, child -> acc + child.combinedFlags }
204+
}
205+
206+
private fun updateEffectFlagsOnChangedEffect(possibleEvent: Any) {
207+
@Suppress("UNCHECKED_CAST")
208+
when (val event = possibleEvent as? ObservableListEvent<Effect> ?: return) {
209+
is ObservableAddEvent -> effectFlags += event.element.value.flags
210+
is ObservableRemoveEvent -> recomputeEffectFlags()
211+
is ObservableClearEvent -> recomputeEffectFlags()
212+
}
213+
}
214+
215+
private fun updateCombinedFlagsOnChangedChild(possibleEvent: Any) {
216+
@Suppress("UNCHECKED_CAST")
217+
when (val event = possibleEvent as? ObservableListEvent<UIComponent> ?: return) {
218+
is ObservableAddEvent -> combinedFlags += event.element.value.combinedFlags
219+
is ObservableRemoveEvent -> recomputeCombinedFlags()
220+
is ObservableClearEvent -> recomputeCombinedFlags()
221+
}
222+
}
223+
//endregion
224+
146225
protected fun requireChildrenUnlocked() {
147226
requireState(childrenLocked == 0, "Cannot modify children while iterating over them.")
148227
}
@@ -564,6 +643,16 @@ abstract class UIComponent : Observable(), ReferenceHolder {
564643
fun beforeChildrenDrawCompat(matrixStack: UMatrixStack) = UMatrixStack.Compat.runLegacyMethod(matrixStack) { beforeChildrenDraw() }
565644

566645
open fun mouseMove(window: Window) {
646+
if (Flags.RequiresMouseMove in ownFlags) {
647+
updateCurrentlyHoveredState(window)
648+
}
649+
650+
if (Flags.RequiresMouseMove in combinedFlags) {
651+
this.forEachChild { it.mouseMove(window) }
652+
}
653+
}
654+
655+
private fun updateCurrentlyHoveredState(window: Window) {
567656
val hovered = isHovered() && window.hoveredFloatingComponent.let {
568657
it == null || it == this || isComponentInParentChain(it)
569658
}
@@ -577,8 +666,6 @@ abstract class UIComponent : Observable(), ReferenceHolder {
577666
this.listener()
578667
currentlyHovered = false
579668
}
580-
581-
this.forEachChild { it.mouseMove(window) }
582669
}
583670

584671
/**
@@ -589,8 +676,6 @@ abstract class UIComponent : Observable(), ReferenceHolder {
589676
open fun mouseClick(mouseX: Double, mouseY: Double, button: Int) {
590677
val clicked = hitTest(mouseX.toFloat(), mouseY.toFloat())
591678

592-
lastDraggedMouseX = mouseX
593-
lastDraggedMouseY = mouseY
594679
lastClickCount = if (System.currentTimeMillis() - lastClickTime < 500) lastClickCount + 1 else 1
595680
lastClickTime = System.currentTimeMillis()
596681

@@ -627,9 +712,6 @@ abstract class UIComponent : Observable(), ReferenceHolder {
627712
for (listener in mouseReleaseListeners)
628713
this.listener()
629714

630-
lastDraggedMouseX = null
631-
lastDraggedMouseY = null
632-
633715
this.forEachChild { it.mouseRelease() }
634716
}
635717

@@ -708,17 +790,17 @@ abstract class UIComponent : Observable(), ReferenceHolder {
708790
}
709791

710792
private inline fun doDragMouse(mouseX: Float, mouseY: Float, button: Int, superCall: UIComponent.() -> Unit) {
711-
if (lastDraggedMouseX == mouseX.toDouble() && lastDraggedMouseY == mouseY.toDouble())
793+
if (Flags.RequiresMouseDrag !in combinedFlags) {
712794
return
795+
}
713796

714-
lastDraggedMouseX = mouseX.toDouble()
715-
lastDraggedMouseY = mouseY.toDouble()
716-
717-
val relativeX = mouseX - getLeft()
718-
val relativeY = mouseY - getTop()
797+
if (Flags.RequiresMouseDrag in ownFlags) {
798+
val relativeX = mouseX - getLeft()
799+
val relativeY = mouseY - getTop()
719800

720-
for (listener in mouseDragListeners)
721-
this.listener(relativeX, relativeY, button)
801+
for (listener in mouseDragListeners)
802+
this.listener(relativeX, relativeY, button)
803+
}
722804

723805
this.forEachChild { it.superCall() }
724806
}
@@ -1540,6 +1622,47 @@ abstract class UIComponent : Observable(), ReferenceHolder {
15401622
return { heldReferences.remove(listener) }
15411623
}
15421624

1625+
@JvmInline
1626+
internal value class Flags(val bits: UInt) {
1627+
operator fun contains(element: Flags) = this.bits and element.bits == element.bits
1628+
infix fun and(other: Flags) = Flags(this.bits and other.bits)
1629+
infix fun or(other: Flags) = Flags(this.bits or other.bits)
1630+
operator fun plus(other: Flags) = this or other
1631+
operator fun minus(other: Flags) = Flags(this.bits and other.bits.inv())
1632+
fun inv() = Flags(bits.inv() and All.bits)
1633+
1634+
companion object {
1635+
private var nextBit = 0
1636+
private val iota: Flags
1637+
get() = Flags(1u shl nextBit++)
1638+
1639+
val None = Flags(0u)
1640+
1641+
val RequiresMouseMove = iota
1642+
val RequiresMouseDrag = iota
1643+
1644+
val All = Flags(iota.bits - 1u)
1645+
1646+
private val cache = ConcurrentHashMap<Class<*>, Flags>().apply {
1647+
put(Effect::class.java, Flags(0u))
1648+
put(UIComponent::class.java, Flags(0u))
1649+
put(Window::class.java, Flags(0u))
1650+
}
1651+
fun initialFor(cls: Class<*>): Flags = cache.getOrPut(cls) {
1652+
flagsBasedOnOverrides(cls) + initialFor(cls.superclass)
1653+
}
1654+
1655+
private fun flagsBasedOnOverrides(cls: Class<*>): Flags = listOf(
1656+
if (cls.overridesMethod("mouseMove", Window::class.java)) RequiresMouseMove else None,
1657+
if (cls.overridesMethod("dragMouse", Int::class.java, Int::class.java, Int::class.java)) RequiresMouseDrag else None,
1658+
if (cls.overridesMethod("dragMouse", Float::class.java, Float::class.java, Int::class.java)) RequiresMouseDrag else None,
1659+
).reduce { acc, flags -> acc + flags }
1660+
1661+
private fun Class<*>.overridesMethod(name: String, vararg args: Class<*>) =
1662+
try { getDeclaredMethod(name, *args); true } catch (_: NoSuchMethodException) { false }
1663+
}
1664+
}
1665+
15431666
companion object {
15441667
// Default value for componentName used as marker for lazy init.
15451668
private val defaultComponentName = String()

src/main/kotlin/gg/essential/elementa/components/Window.kt

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ class Window @JvmOverloads constructor(
223223
// 2 and over. See [ElementaVersion.V2] for more info.
224224
val (adjustedX, adjustedY) = pixelCoordinatesToPixelCenter(mouseX, mouseY)
225225

226+
prevDraggedMouseX = adjustedX.toFloat()
227+
prevDraggedMouseY = adjustedY.toFloat()
228+
226229
doMouseClick(adjustedX, adjustedY, button)
227230
}
228231

@@ -270,6 +273,9 @@ class Window @JvmOverloads constructor(
270273

271274
super.mouseRelease()
272275

276+
prevDraggedMouseX = null
277+
prevDraggedMouseY = null
278+
273279
currentMouseButton = -1
274280
}
275281

@@ -291,14 +297,25 @@ class Window @JvmOverloads constructor(
291297
}
292298
}
293299

300+
internal var prevDraggedMouseX: Float? = null
301+
internal var prevDraggedMouseY: Float? = null
302+
294303
override fun animationFrame() {
295304
if (currentMouseButton != -1) {
296305
val (mouseX, mouseY) = getMousePosition()
297306
if (version >= ElementaVersion.v2) {
298-
dragMouse(mouseX, mouseY, currentMouseButton)
307+
if (prevDraggedMouseX != mouseX && prevDraggedMouseY != mouseY) {
308+
prevDraggedMouseX = mouseX
309+
prevDraggedMouseY = mouseY
310+
dragMouse(mouseX, mouseY, currentMouseButton)
311+
}
299312
} else {
300-
@Suppress("DEPRECATION")
301-
dragMouse(mouseX.toInt(), mouseY.toInt(), currentMouseButton)
313+
if (prevDraggedMouseX != mouseX.toInt().toFloat() && prevDraggedMouseY != mouseY.toInt().toFloat()) {
314+
prevDraggedMouseX = mouseX.toInt().toFloat()
315+
prevDraggedMouseY = mouseY.toInt().toFloat()
316+
@Suppress("DEPRECATION")
317+
dragMouse(mouseX.toInt(), mouseY.toInt(), currentMouseButton)
318+
}
302319
}
303320
}
304321

src/main/kotlin/gg/essential/elementa/effects/Effect.kt

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

33
import gg.essential.elementa.UIComponent
4+
import gg.essential.elementa.UIComponent.Flags
45
import gg.essential.elementa.components.UpdateFunc
56
import gg.essential.universal.UMatrixStack
67

@@ -10,6 +11,20 @@ import gg.essential.universal.UMatrixStack
1011
* This is where you can affect any drawing done.
1112
*/
1213
abstract class Effect {
14+
internal var flags: Flags = Flags.initialFor(javaClass)
15+
set(newValue) {
16+
val oldValue = field
17+
if (oldValue == newValue) return
18+
field = newValue
19+
updateFuncParent?.let { parent ->
20+
if (oldValue in newValue) { // merely additions?
21+
parent.effectFlags += newValue
22+
} else {
23+
parent.recomputeEffectFlags()
24+
}
25+
}
26+
}
27+
1328
protected lateinit var boundComponent: UIComponent
1429

1530
fun bindComponent(component: UIComponent) {

0 commit comments

Comments
 (0)