diff --git a/src/main/kotlin/gg/essential/elementa/UIComponent.kt b/src/main/kotlin/gg/essential/elementa/UIComponent.kt index 292a6d57..bc2131c0 100644 --- a/src/main/kotlin/gg/essential/elementa/UIComponent.kt +++ b/src/main/kotlin/gg/essential/elementa/UIComponent.kt @@ -25,6 +25,7 @@ import gg.essential.universal.UResolution import org.lwjgl.opengl.GL11 import java.awt.Color import java.util.* +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedDeque import java.util.concurrent.CopyOnWriteArrayList import java.util.function.BiConsumer @@ -55,6 +56,7 @@ abstract class UIComponent : Observable(), ReferenceHolder { val effects: MutableList = mutableListOf().observable().apply { addObserver { _, event -> updateUpdateFuncsOnChangedEffect(event) + updateEffectFlagsOnChangedEffect(event) } } @@ -65,6 +67,7 @@ abstract class UIComponent : Observable(), ReferenceHolder { setWindowCacheOnChangedChild(event) updateFloatingComponentsOnChangedChild(event) updateUpdateFuncsOnChangedChild(event) + updateCombinedFlagsOnChangedChild(event) } } @@ -80,8 +83,14 @@ abstract class UIComponent : Observable(), ReferenceHolder { notifyObservers(constraints) } - var lastDraggedMouseX: Double? = null - var lastDraggedMouseY: Double? = null + @Deprecated("This property should have been private and probably does not do what you expect it to.") + var lastDraggedMouseX: Double? + get() = Window.ofOrNull(this)?.prevDraggedMouseX?.toDouble() + set(_) {} + @Deprecated("This property should have been private and probably does not do what you expect it to.") + var lastDraggedMouseY: Double? + get() = Window.ofOrNull(this)?.prevDraggedMouseY?.toDouble() + set(_) {} /* Bubbling Events */ var mouseScrollListeners = mutableListOf Unit>() @@ -92,8 +101,11 @@ abstract class UIComponent : Observable(), ReferenceHolder { /* Non-Bubbling Events */ val mouseReleaseListeners = mutableListOf Unit>() val mouseEnterListeners = mutableListOf Unit>() + get() = field.also { ownFlags += Flags.RequiresMouseMove } val mouseLeaveListeners = mutableListOf Unit>() + get() = field.also { ownFlags += Flags.RequiresMouseMove } val mouseDragListeners = mutableListOf Unit>() + get() = field.also { ownFlags += Flags.RequiresMouseDrag } val keyTypedListeners = mutableListOf Unit>() private var currentlyHovered = false @@ -142,6 +154,73 @@ abstract class UIComponent : Observable(), ReferenceHolder { children.forEach { it.recursivelySetWindowCache(window) } } + //region Internal flags tracking + /** Flags which apply to this component specifically. */ + internal var ownFlags = Flags.initialFor(javaClass) + set(newValue) { + val oldValue = field + if (oldValue == newValue) return + field = newValue + if (oldValue in newValue) { // merely additions? + combinedFlags += newValue + } else { + recomputeCombinedFlags() + } + } + /** Flags which apply to one of the effects of tis component. */ + internal var effectFlags = Flags(0u) + set(newValue) { + val oldValue = field + if (oldValue == newValue) return + field = newValue + if (oldValue in newValue) { // merely additions? + combinedFlags += newValue + } else { + recomputeCombinedFlags() + } + } + /** Combined flags of this component, its effects, and its children. */ + internal var combinedFlags = ownFlags + set(newValue) { + val oldValue = field + if (oldValue == newValue) return + field = newValue + if (hasParent && parent != this) { + if (oldValue in newValue) { // merely additions? + parent.combinedFlags += newValue + } else { + parent.recomputeCombinedFlags() + } + } + } + + internal fun recomputeEffectFlags() { + effectFlags = effects.fold(Flags(0u)) { acc, effect -> acc + effect.flags } + } + + private fun recomputeCombinedFlags() { + combinedFlags = children.fold(ownFlags + effectFlags) { acc, child -> acc + child.combinedFlags } + } + + private fun updateEffectFlagsOnChangedEffect(possibleEvent: Any) { + @Suppress("UNCHECKED_CAST") + when (val event = possibleEvent as? ObservableListEvent ?: return) { + is ObservableAddEvent -> effectFlags += event.element.value.flags + is ObservableRemoveEvent -> recomputeEffectFlags() + is ObservableClearEvent -> recomputeEffectFlags() + } + } + + private fun updateCombinedFlagsOnChangedChild(possibleEvent: Any) { + @Suppress("UNCHECKED_CAST") + when (val event = possibleEvent as? ObservableListEvent ?: return) { + is ObservableAddEvent -> combinedFlags += event.element.value.combinedFlags + is ObservableRemoveEvent -> recomputeCombinedFlags() + is ObservableClearEvent -> recomputeCombinedFlags() + } + } + //endregion + protected fun requireChildrenUnlocked() { requireState(childrenLocked == 0, "Cannot modify children while iterating over them.") } @@ -563,6 +642,16 @@ abstract class UIComponent : Observable(), ReferenceHolder { fun beforeChildrenDrawCompat(matrixStack: UMatrixStack) = UMatrixStack.Compat.runLegacyMethod(matrixStack) { beforeChildrenDraw() } open fun mouseMove(window: Window) { + if (Flags.RequiresMouseMove in ownFlags) { + updateCurrentlyHoveredState(window) + } + + if (Flags.RequiresMouseMove in combinedFlags) { + this.forEachChild { it.mouseMove(window) } + } + } + + private fun updateCurrentlyHoveredState(window: Window) { val hovered = isHovered() && window.hoveredFloatingComponent.let { it == null || it == this || isComponentInParentChain(it) } @@ -576,8 +665,6 @@ abstract class UIComponent : Observable(), ReferenceHolder { this.listener() currentlyHovered = false } - - this.forEachChild { it.mouseMove(window) } } /** @@ -588,8 +675,6 @@ abstract class UIComponent : Observable(), ReferenceHolder { open fun mouseClick(mouseX: Double, mouseY: Double, button: Int) { val clicked = hitTest(mouseX.toFloat(), mouseY.toFloat()) - lastDraggedMouseX = mouseX - lastDraggedMouseY = mouseY lastClickCount = if (System.currentTimeMillis() - lastClickTime < 500) lastClickCount + 1 else 1 lastClickTime = System.currentTimeMillis() @@ -626,9 +711,6 @@ abstract class UIComponent : Observable(), ReferenceHolder { for (listener in mouseReleaseListeners) this.listener() - lastDraggedMouseX = null - lastDraggedMouseY = null - this.forEachChild { it.mouseRelease() } } @@ -707,17 +789,17 @@ abstract class UIComponent : Observable(), ReferenceHolder { } private inline fun doDragMouse(mouseX: Float, mouseY: Float, button: Int, superCall: UIComponent.() -> Unit) { - if (lastDraggedMouseX == mouseX.toDouble() && lastDraggedMouseY == mouseY.toDouble()) + if (Flags.RequiresMouseDrag !in combinedFlags) { return + } - lastDraggedMouseX = mouseX.toDouble() - lastDraggedMouseY = mouseY.toDouble() - - val relativeX = mouseX - getLeft() - val relativeY = mouseY - getTop() + if (Flags.RequiresMouseDrag in ownFlags) { + val relativeX = mouseX - getLeft() + val relativeY = mouseY - getTop() - for (listener in mouseDragListeners) - this.listener(relativeX, relativeY, button) + for (listener in mouseDragListeners) + this.listener(relativeX, relativeY, button) + } this.forEachChild { it.superCall() } } @@ -1520,6 +1602,47 @@ abstract class UIComponent : Observable(), ReferenceHolder { return { heldReferences.remove(listener) } } + @JvmInline + internal value class Flags(val bits: UInt) { + operator fun contains(element: Flags) = this.bits and element.bits == element.bits + infix fun and(other: Flags) = Flags(this.bits and other.bits) + infix fun or(other: Flags) = Flags(this.bits or other.bits) + operator fun plus(other: Flags) = this or other + operator fun minus(other: Flags) = Flags(this.bits and other.bits.inv()) + fun inv() = Flags(bits.inv() and All.bits) + + companion object { + private var nextBit = 0 + private val iota: Flags + get() = Flags(1u shl nextBit++) + + val None = Flags(0u) + + val RequiresMouseMove = iota + val RequiresMouseDrag = iota + + val All = Flags(iota.bits - 1u) + + private val cache = ConcurrentHashMap, Flags>().apply { + put(Effect::class.java, Flags(0u)) + put(UIComponent::class.java, Flags(0u)) + put(Window::class.java, Flags(0u)) + } + fun initialFor(cls: Class<*>): Flags = cache.getOrPut(cls) { + flagsBasedOnOverrides(cls) + initialFor(cls.superclass) + } + + private fun flagsBasedOnOverrides(cls: Class<*>): Flags = listOf( + 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, + ).reduce { acc, flags -> acc + flags } + + private fun Class<*>.overridesMethod(name: String, vararg args: Class<*>) = + try { getDeclaredMethod(name, *args); true } catch (_: NoSuchMethodException) { false } + } + } + companion object { // Default value for componentName used as marker for lazy init. private val defaultComponentName = String() diff --git a/src/main/kotlin/gg/essential/elementa/components/Window.kt b/src/main/kotlin/gg/essential/elementa/components/Window.kt index 902495fc..bb14554c 100644 --- a/src/main/kotlin/gg/essential/elementa/components/Window.kt +++ b/src/main/kotlin/gg/essential/elementa/components/Window.kt @@ -223,6 +223,9 @@ class Window @JvmOverloads constructor( // 2 and over. See [ElementaVersion.V2] for more info. val (adjustedX, adjustedY) = pixelCoordinatesToPixelCenter(mouseX, mouseY) + prevDraggedMouseX = adjustedX.toFloat() + prevDraggedMouseY = adjustedY.toFloat() + doMouseClick(adjustedX, adjustedY, button) } @@ -270,6 +273,9 @@ class Window @JvmOverloads constructor( super.mouseRelease() + prevDraggedMouseX = null + prevDraggedMouseY = null + currentMouseButton = -1 } @@ -291,14 +297,25 @@ class Window @JvmOverloads constructor( } } + internal var prevDraggedMouseX: Float? = null + internal var prevDraggedMouseY: Float? = null + override fun animationFrame() { if (currentMouseButton != -1) { val (mouseX, mouseY) = getMousePosition() if (version >= ElementaVersion.v2) { - dragMouse(mouseX, mouseY, currentMouseButton) + if (prevDraggedMouseX != mouseX && prevDraggedMouseY != mouseY) { + prevDraggedMouseX = mouseX + prevDraggedMouseY = mouseY + dragMouse(mouseX, mouseY, currentMouseButton) + } } else { - @Suppress("DEPRECATION") - dragMouse(mouseX.toInt(), mouseY.toInt(), currentMouseButton) + if (prevDraggedMouseX != mouseX.toInt().toFloat() && prevDraggedMouseY != mouseY.toInt().toFloat()) { + prevDraggedMouseX = mouseX.toInt().toFloat() + prevDraggedMouseY = mouseY.toInt().toFloat() + @Suppress("DEPRECATION") + dragMouse(mouseX.toInt(), mouseY.toInt(), currentMouseButton) + } } } diff --git a/src/main/kotlin/gg/essential/elementa/effects/Effect.kt b/src/main/kotlin/gg/essential/elementa/effects/Effect.kt index c8cfc1e0..05f2fbab 100644 --- a/src/main/kotlin/gg/essential/elementa/effects/Effect.kt +++ b/src/main/kotlin/gg/essential/elementa/effects/Effect.kt @@ -1,6 +1,7 @@ package gg.essential.elementa.effects import gg.essential.elementa.UIComponent +import gg.essential.elementa.UIComponent.Flags import gg.essential.elementa.components.UpdateFunc import gg.essential.universal.UMatrixStack @@ -10,6 +11,20 @@ import gg.essential.universal.UMatrixStack * This is where you can affect any drawing done. */ abstract class Effect { + internal var flags: Flags = Flags.initialFor(javaClass) + set(newValue) { + val oldValue = field + if (oldValue == newValue) return + field = newValue + updateFuncParent?.let { parent -> + if (oldValue in newValue) { // merely additions? + parent.effectFlags += newValue + } else { + parent.recomputeEffectFlags() + } + } + } + protected lateinit var boundComponent: UIComponent fun bindComponent(component: UIComponent) {