All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.lehaine.littlekt.graph.SceneGraph.kt Maven / Gradle / Ivy

There is a newer version: 0.9.0
Show newest version
package com.lehaine.littlekt.graph

import com.lehaine.littlekt.Context
import com.lehaine.littlekt.Disposable
import com.lehaine.littlekt.graph.node.*
import com.lehaine.littlekt.graph.node.annotation.SceneGraphDslMarker
import com.lehaine.littlekt.graph.node.render.Material
import com.lehaine.littlekt.graph.node.resource.InputEvent
import com.lehaine.littlekt.graph.node.ui.Control
import com.lehaine.littlekt.graphics.OrthographicCamera
import com.lehaine.littlekt.graphics.Textures
import com.lehaine.littlekt.graphics.g2d.Batch
import com.lehaine.littlekt.graphics.g2d.SpriteBatch
import com.lehaine.littlekt.graphics.g2d.TextureSlice
import com.lehaine.littlekt.graphics.g2d.shape.ShapeRenderer
import com.lehaine.littlekt.graphics.gl.BlendEquationMode
import com.lehaine.littlekt.graphics.gl.BlendFactor
import com.lehaine.littlekt.graphics.gl.FaceMode
import com.lehaine.littlekt.graphics.gl.State
import com.lehaine.littlekt.input.*
import com.lehaine.littlekt.math.MutableVec2f
import com.lehaine.littlekt.util.datastructure.Pool
import com.lehaine.littlekt.util.fastForEach
import com.lehaine.littlekt.util.milliseconds
import com.lehaine.littlekt.util.seconds
import com.lehaine.littlekt.util.viewport.ScreenViewport
import com.lehaine.littlekt.util.viewport.Viewport
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds

/**
 * Create a new scene graph with a [callback] with the [SceneGraph] in context.
 * @param context the current context
 * @param viewport the viewport that the camera of the scene graph will own
 * @param batch an option sprite batch. If omitted, the scene graph will create and manage its own.
 * @param callback the callback that is invoked with a [SceneGraph] context
 * in order to initialize any values and create nodes
 * @return the newly created [SceneGraph]
 */
@OptIn(ExperimentalContracts::class)
inline fun sceneGraph(
    context: Context,
    viewport: Viewport = ScreenViewport(
        context.graphics.width,
        context.graphics.height
    ),
    batch: Batch? = null,
    controller: InputMapController? = null,
    whitePixel: TextureSlice = Textures.white,
    callback: @SceneGraphDslMarker SceneGraph.() -> Unit = {},
): SceneGraph {
    contract { callsInPlace(callback, InvocationKind.EXACTLY_ONCE) }
    val signals = SceneGraph.UiInputSignals(
        "ui_accept",
        "ui_select",
        "ui_cancel",
        "ui_focus_next",
        "ui_focus_prev",
        "ui_left",
        "ui_right",
        "ui_up",
        "ui_down",
        "ui_home",
        "ui_end"
    )
    return SceneGraph(
        context,
        viewport,
        batch,
        signals,
        controller ?: createDefaultSceneGraphController(context.input, signals),
        whitePixel
    ).also(callback)
}

/**
 * Create a new scene graph with a [callback] with the [SceneGraph] in context.
 * @param context the current context
 * @param viewport the viewport that the camera of the scene graph will own
 * @param batch an option sprite batch. If omitted, the scene graph will create and manage its own.
 * @param callback the callback that is invoked with a [SceneGraph] context
 * in order to initialize any values and create nodes
 * @return the newly created [SceneGraph]
 */
@OptIn(ExperimentalContracts::class)
inline fun  sceneGraph(
    context: Context,
    viewport: Viewport = ScreenViewport(
        context.graphics.width,
        context.graphics.height
    ),
    batch: Batch? = null,
    uiInputSignals: SceneGraph.UiInputSignals = SceneGraph.UiInputSignals(),
    controller: InputMapController = InputMapController(context.input),
    whitePixel: TextureSlice = Textures.white,
    callback: @SceneGraphDslMarker SceneGraph.() -> Unit = {},
): SceneGraph {
    contract { callsInPlace(callback, InvocationKind.EXACTLY_ONCE) }
    return SceneGraph(
        context,
        viewport,
        batch,
        uiInputSignals,
        controller,
        whitePixel
    ).also(callback)
}

fun  InputMapController.addDefaultUiInput(uiInputSignals: SceneGraph.UiInputSignals) {
    uiInputSignals.uiAccept?.let {
        addBinding(
            it,
            keys = listOf(Key.SPACE, Key.ENTER),
            buttons = listOf(GameButton.XBOX_A)
        )
    }
    uiInputSignals.uiSelect?.let { addBinding(it, keys = listOf(Key.SPACE), buttons = listOf(GameButton.XBOX_Y)) }
    uiInputSignals.uiCancel?.let { addBinding(it, keys = listOf(Key.ESCAPE), buttons = listOf(GameButton.XBOX_B)) }
    uiInputSignals.uiFocusNext?.let { addBinding(it, keys = listOf(Key.TAB)) }
    uiInputSignals.uiFocusPrev?.let {
        addBinding(
            it,
            keys = listOf(Key.TAB),
            keyModifiers = listOf(InputMapController.KeyModifier.SHIFT)
        )
    }
    uiInputSignals.uiUp?.let { addBinding(it, keys = listOf(Key.ARROW_UP), buttons = listOf(GameButton.UP)) }
    uiInputSignals.uiDown?.let { addBinding(it, keys = listOf(Key.ARROW_DOWN), buttons = listOf(GameButton.DOWN)) }
    uiInputSignals.uiLeft?.let { addBinding(it, keys = listOf(Key.ARROW_LEFT), buttons = listOf(GameButton.LEFT)) }
    uiInputSignals.uiRight?.let {
        addBinding(
            it,
            keys = listOf(Key.ARROW_RIGHT),
            buttons = listOf(GameButton.RIGHT)
        )
    }
    uiInputSignals.uiHome?.let { addBinding(it, keys = listOf(Key.HOME)) }
    uiInputSignals.uiEnd?.let { addBinding(it, keys = listOf(Key.END)) }
}

fun  createDefaultSceneGraphController(
    input: Input,
    uiInputSignals: SceneGraph.UiInputSignals,
): InputMapController =
    InputMapController(input).also { it.addDefaultUiInput(uiInputSignals) }

/**
 * A class for creating a scene graph of nodes.
 * @param context the current context
 * @param viewport the viewport that the camera of the scene graph will own
 * @param batch an option sprite batch. If omitted, the scene graph will create and manage its own.
 * @param uiInputSignals the input signals mapped to the UI input of type [InputType].
 * @param controller the input map controller for the scene graph
 * @param whitePixel a white 1x1 pixel [TextureSlice] that is used for rendering with [ShapeRenderer].
 * @author Colton Daily
 * @date 1/1/2022
 */
open class SceneGraph(
    val context: Context,
    viewport: Viewport = ScreenViewport(
        context.graphics.width,
        context.graphics.height
    ),
    batch: Batch? = null,
    val uiInputSignals: UiInputSignals = UiInputSignals(),
    val controller: InputMapController = createDefaultSceneGraphController(
        context.input,
        uiInputSignals
    ),
    whitePixel: TextureSlice = Textures.white,
) : InputMapProcessor, Disposable {
    private var ownsBatch = true
    val batch: Batch = batch?.also { ownsBatch = false } ?: SpriteBatch(context)
    val shapeRenderer: ShapeRenderer = ShapeRenderer(this.batch, whitePixel)

    /**
     * The root [ViewportCanvasLayer] that is used for rendering all the children in the graph. Do not add children
     * directly to this node. Instead, add children to the [root] node.
     */
    val sceneCanvas: ViewportCanvasLayer by lazy {
        ViewportCanvasLayer().apply {
            name = "Scene Viewport"
            this.viewport = viewport
        }
    }

    /**
     * The root node that should be used to add any children nodes to.
     */
    val root: Node by lazy {
        Node().apply { name = "Root" }.addTo(sceneCanvas)
    }

    /**
     * The virtual width of the [sceneCanvas].
     */
    val width: Float get() = sceneCanvas.virtualWidth

    /**
     * The virtual height of hte [sceneCanvas].
     */
    val height: Float get() = sceneCanvas.virtualHeight

    /**
     * The target FPS for [tmod].
     */
    var targetFPS = 60

    /**
     * The time modifier based off of [targetFPS].
     *
     * If [targetFPS] is set to `60` and the application is running at `120` FPS then this value will be `0.5f`
     * This can be used instead of [dt] to handle frame independent logic.
     */
    var tmod: Float = 1f
        private set

    /**
     * Pixel Per Unit. Changing this value affects [ppuInv]. Defaults to `1`.
     */
    open var ppu = 1f

    /**
     * The inverse of [ppu]. Can be used to scale nodes correctly when using a [ppu] that isn't `1`.
     */
    val ppuInv get() = 1f / ppu

    /**
     * The current delta time.
     */
    var dt: Duration = Duration.ZERO
        private set

    /**
     * The fixed progression lerp ratio for fixed updates. This is used for rendering nodes that
     * use [Node.fixedUpdate] for movement / physics logic.
     */
    val fixedProgressionRatio: Float get() = _fixedProgressionRatio

    /**
     * The interval for [Node.fixedUpdate] to fire. Defaults to `30` times per second.
     */
    var fixedTimesPerSecond: Int = 30
        set(value) {
            field = value
            time = (1f / value).seconds
        }

    /**
     * When `true`, nodes will handle rendering debug related info such as node bounds.
     * When `false` nodes will not be rendered.
     *
     * This will take into effect after the current frame finishes.
     */
    var requestShowDebugInfo = false
        set(value) {
            if (field == value) return
            field = value
            debugInfoDirty = true
        }

    private var debugInfoDirty = false

    /**
     * This is the true current value if debugging is rendering.
     * To change this value see [requestShowDebugInfo].
     */
    var showDebugInfo: Boolean = false
        private set

    private var accum = 0.milliseconds
    private var _fixedProgressionRatio = 1f
    private var time = (1f / fixedTimesPerSecond).seconds

    /**
     * Holds the current [Material] of the last rendered [Node] if no changes were made)
     */
    var currentMaterial: Material? = null

    /**
     * The current frame count.
     */
    val frame: Int get() = frameCount

    private var frameCount = 0

    // scene input related fields
    private var mouseScreenX: Float = 0f
    private var mouseScreenY: Float = 0f
    private var mouseOverControl: Control? = null
    private var keyboardFocus: Control? = null
    private val touchFocusPool = Pool(reset = { it.reset() }, preallocate = 1) { TouchFocus() }
    private val inputEventPool = Pool(reset = { it.reset() }, preallocate = 10) { InputEvent() }
    private val touchFocuses = ArrayList(4)
    private val pointerScreenX = FloatArray(20)
    private val pointerScreenY = FloatArray(20)
    private val pointerOverControls = arrayOfNulls(20)
    private val pointerTouched = BooleanArray(20)
    private val viewportsApplied = mutableListOf()

    private val tempVec = MutableVec2f()

    private var initialized = false

    private val unhandledInputQueue = ArrayDeque>(20)

    /**
     * Resizes the internal graph's [OrthographicCamera] and [CanvasLayer].
     * @param centerCamera if true will center the graphs internal camera after resizing the viewport
     */
    open fun resize(width: Int, height: Int, centerCamera: Boolean = false) {
        sceneCanvas.propagateResize(width, height, centerCamera)
    }


    /**
     * Initializes the root [Node] and [InputProcessor]. This must be called before an [update] or [render] calls.
     */
    open suspend fun initialize() {
        controller.addInputMapProcessor(this)
        context.input.addInputProcessor(this)
        sceneCanvas.scene = this
        root.initialize()
        onStart()
        initialized = true
    }

    /**
     * Renders the entire tree.
     */
    open fun render() {
        if (!initialized) error("You need to call 'initialize()'once before doing any rendering or updating!")
        sceneCanvas.render(batch, shapeRenderer) { node, _, _, _, _ -> checkNodeMaterial(node) }
        end()

        if (debugInfoDirty) {
            showDebugInfo = requestShowDebugInfo
            debugInfoDirty = false
        }
    }

    private fun checkNodeMaterial(node: Node) {
        if (node !is CanvasItem) return

        var materialChanged = false
        // check for Material changes
        if (node.material != currentMaterial) {
            currentMaterial = node.material
            materialChanged = true
        }

        if (materialChanged) {
            currentMaterial?.let { mat ->
                mat.shader?.let {
                    mat.onPreRender()
                }
            }
            flush()
        }
    }

    protected fun flush() {
        val wasDrawing = batch.drawing
        end()

        batch.useDefaultShader()
        currentMaterial?.let { mat ->
            setMaterialGlFunctions(mat)
            mat.shader?.bind()
        }
        if (wasDrawing) {
            batch.begin()
        }
    }

    protected fun end() {
        if (batch.drawing) batch.end()
        batch.setBlendFunctionSeparate(
            BlendFactor.SRC_ALPHA,
            BlendFactor.ONE_MINUS_SRC_ALPHA,
            BlendFactor.SRC_ALPHA,
            BlendFactor.ONE_MINUS_SRC_ALPHA
        )
        val gl = context.gl
        gl.blendEquationSeparate(BlendEquationMode.FUNC_ADD, BlendEquationMode.FUNC_ADD)
    }

    private fun setMaterialGlFunctions(material: Material) {
        val gl = context.gl
        val blendMode = material.blendMode
        val depthStencilMode = material.depthStencilMode

        gl.blendEquationSeparate(blendMode.colorBlendFunction, blendMode.alphaBlendFunction)
        batch.setBlendFunctionSeparate(
            blendMode.colorSourceBlend,
            blendMode.colorDestinationBlend,
            blendMode.alphaSourceBlend,
            blendMode.alphaDestinationBlend
        )

        if (depthStencilMode.depthBufferEnable) {
            gl.enable(State.DEPTH_TEST)
            gl.depthFunc(depthStencilMode.depthBufferFunction)
            gl.depthMask(true)
        }

        if (depthStencilMode.stencilEnable) {
            gl.enable(State.STENCIL_TEST)
            gl.stencilFuncSeparate(
                FaceMode.FRONT,
                depthStencilMode.stencilFunction,
                depthStencilMode.referenceStencil,
                depthStencilMode.stencilMask
            )
            gl.stencilOpSeparate(
                FaceMode.FRONT,
                depthStencilMode.stencilFail,
                depthStencilMode.stencilDepthBufferFail,
                depthStencilMode.stencilPass
            )
        }
    }

    /**
     * Lifecycle method. This is called whenever the [SceneGraph] is set before [initialize] is called.
     * Any nodes added to this [Node] context won't be added until the next frame update.
     */
    open suspend fun Node.initialize() = Unit

    /**
     * Lifecycle method. This is called when this scene becomes the active scene.
     */
    open fun onStart() = Unit

    /**
     * Open method that is triggered whenever a [Control] node receives an input event.
     */
    open fun uiInput(control: Control, event: InputEvent) {}

    /**
     * Request a [Control] to receive keyboard focus.
     */
    fun requestFocus(control: Control) {
        if (keyboardFocus == control) return
        if (!control.enabled) return
        val oldFocus = keyboardFocus
        keyboardFocus = control
        oldFocus?._onFocusLost()
        control._onFocus()
    }

    /**
     * Releases any current keyboard focus.
     */
    fun releaseFocus() {
        val control = keyboardFocus
        keyboardFocus = null
        control?._onFocusLost()
    }

    /**
     * Checks if the [Control] has the current keyboard focus.
     */
    fun hasFocus(control: Control) = keyboardFocus == control

    /**
     * Updates all the nodes in the tree.
     */
    open fun update(dt: Duration) {
        if (!initialized) error("You need to call 'initialize()' once before doing any rendering or updating!")
        this.dt = dt
        tmod = dt.seconds * targetFPS
        pointerOverControls.forEachIndexed { index, overLast ->
            if (!pointerTouched[index]) {
                if (overLast != null) {
                    pointerOverControls[index] = null
                    screenToSceneCoordinates(
                        tempVec.set(
                            pointerScreenX[index],
                            pointerScreenY[index]
                        )
                    )
                    val event = inputEventPool.alloc().apply {
                        sceneX = tempVec.x
                        sceneY = tempVec.y
                        overLast.canvas?.screenToCanvasCoordinates(tempVec)
                        canvasX = tempVec.x
                        canvasY = tempVec.y
                        overLast.toLocal(tempVec, tempVec)
                        localX = tempVec.x
                        localY = tempVec.y
                        type = InputEvent.Type.MOUSE_EXIT
                        pointer = Pointer.cache[index]
                    }
                    overLast.callUiInput(event)
                    uiInput(overLast, event)
                    inputEventPool.free(event)
                }
                return@forEachIndexed
            }
            pointerOverControls[index] =
                fireEnterAndExit(overLast, pointerScreenX[index], pointerScreenY[index], Pointer.cache[index])
        }

        when (context.platform) {
            Context.Platform.DESKTOP, Context.Platform.WEBGL, Context.Platform.WEBGL2 -> {
                mouseOverControl = fireEnterAndExit(mouseOverControl, mouseScreenX, mouseScreenY, Pointer.POINTER1)
            }

            else -> {
                // do nothing
            }
        }

        unhandledInputQueue.forEach {
            callUnhandledInput(it)
            inputEventPool.free(it)
        }
        unhandledInputQueue.clear()

        accum += dt
        while (accum >= time) {
            accum -= time
            if (root.enabled && (root.updateInterval == 1 || frameCount % root.updateInterval == 0)) {
                root.propagateFixedUpdate()
            }
        }

        _fixedProgressionRatio = accum.milliseconds / time.milliseconds

        if (root.enabled && (root.updateInterval == 1 || frameCount % root.updateInterval == 0)) {
            root.propagatePreUpdate()
            root.propagateUpdate()
            root.propagatePostUpdate()
        }
        frameCount++
    }

    internal fun pushViewport(viewport: Viewport) {
        viewportsApplied += viewport
        viewport.apply(context)
    }

    internal fun popViewport() {
        if (viewportsApplied.isNotEmpty()) {
            viewportsApplied.removeLast()
            if (viewportsApplied.isNotEmpty()) {
                viewportsApplied.last().apply(context)
            }
        }
    }

    internal fun hasMultipleViewportsApplied() = viewportsApplied.size >= 2

    override fun touchDown(screenX: Float, screenY: Float, pointer: Pointer): Boolean {
        if (!isInsideViewport(screenX.toInt(), screenY.toInt())) return false
        if (controller.touchDown(screenX, screenY, pointer)) return true

        pointerTouched[pointer.ordinal] = true
        pointerScreenX[pointer.ordinal] = screenX
        pointerScreenY[pointer.ordinal] = screenY

        screenToSceneCoordinates(tempVec.set(screenX, screenY))

        val sceneX = tempVec.x
        val sceneY = tempVec.y

        val event = inputEventPool.alloc().apply {
            type = InputEvent.Type.TOUCH_DOWN
            this.sceneX = sceneX
            this.sceneY = sceneY
            this.pointer = pointer
        }

        // InputEvents go: input -> ui input -> unhandled input
        if (callInput(event)) {
            inputEventPool.free(event)
            return true
        }

        val target = callHitTest(tempVec.x, tempVec.y)
        target?.let {
            if (pointer == Pointer.MOUSE_LEFT && it.focusMode != Control.FocusMode.NONE) {
                it.grabFocus()
                keyboardFocus = it
            }
            it.callUiInput(event)
            uiInput(it, event)
            addTouchFocus(it, pointer)
        }
        if (event.handled) {
            inputEventPool.free(event)
            return true
        }

        unhandledInputQueue += event
        return false
    }

    override fun touchUp(screenX: Float, screenY: Float, pointer: Pointer): Boolean {
        if (controller.touchUp(screenX, screenY, pointer)) return true
        pointerTouched[pointer.ordinal] = false
        pointerScreenX[pointer.ordinal] = screenX
        pointerScreenY[pointer.ordinal] = screenY

        screenToSceneCoordinates(tempVec.set(screenX, screenY))

        val sceneX = tempVec.x
        val sceneY = tempVec.y

        val event = inputEventPool.alloc().apply {
            type = InputEvent.Type.TOUCH_UP
            this.sceneX = sceneX
            this.sceneY = sceneY
            this.pointer = pointer
        }

        // InputEvents go: input -> ui input -> unhandled input
        if (callInput(event)) {
            inputEventPool.free(event)
            return true
        }

        touchFocuses.fastForEach { focus ->
            if (focus.pointer != pointer) {
                return@fastForEach
            }
            if (!touchFocuses.remove(focus)) { // focus already gone
                return@fastForEach
            }
            focus.target?.let {
                it.toLocal(sceneX, sceneY, tempVec)
                event.apply {
                    localX = tempVec.x
                    localY = tempVec.y
                }
                it.callUiInput(event)
                uiInput(it, event)
            }
            touchFocusPool.free(focus)
        }

        if (event.handled) {
            inputEventPool.free(event)
            return true
        }

        unhandledInputQueue += event
        return false
    }

    override fun touchDragged(screenX: Float, screenY: Float, pointer: Pointer): Boolean {
        if (controller.touchDragged(screenX, screenY, pointer)) return true

        pointerScreenX[pointer.ordinal] = screenX
        pointerScreenY[pointer.ordinal] = screenY
        mouseScreenX = screenX
        mouseScreenY = screenY

        if (touchFocuses.isEmpty()) {
            return false
        }

        screenToSceneCoordinates(tempVec.set(screenX, screenY))

        val sceneX = tempVec.x
        val sceneY = tempVec.y

        val event = inputEventPool.alloc().apply {
            type = InputEvent.Type.TOUCH_DRAGGED
            this.sceneX = sceneX
            this.sceneY = sceneY
            this.pointer = pointer
        }

        // InputEvents go: input -> ui input -> unhandled input
        if (callInput(event)) {
            inputEventPool.free(event)
            return true
        }

        touchFocuses.fastForEach { focus ->
            if (focus.pointer != pointer) {
                return@fastForEach
            }
            if (!touchFocuses.contains(focus)) { // focus already gone
                return@fastForEach
            }
            focus.target?.let {
                it.toLocal(sceneX, sceneY, tempVec)
                event.apply {
                    localX = tempVec.x
                    localY = tempVec.y
                }
                it.callUiInput(event)
                uiInput(it, event)
            }
        }

        if (event.handled) {
            inputEventPool.free(event)
            return true
        }

        unhandledInputQueue += event
        return false
    }

    override fun mouseMoved(screenX: Float, screenY: Float): Boolean {
        if (controller.mouseMoved(screenX, screenY)) return true

        mouseScreenX = screenX
        mouseScreenY = screenY

        screenToSceneCoordinates(tempVec.set(screenX, screenY))

        val sceneX = tempVec.x
        val sceneY = tempVec.y

        val event = inputEventPool.alloc().apply {
            type = InputEvent.Type.MOUSE_HOVER
            this.sceneX = sceneX
            this.sceneY = sceneY
        }

        // InputEvents go: input -> ui input -> unhandled input
        if (callInput(event)) {
            inputEventPool.free(event)
            return true
        }

        mouseOverControl?.let {
            it.toLocal(sceneX, sceneY, tempVec)
            event.apply {
                localX = tempVec.x
                localY = tempVec.y
            }
            it.callUiInput(event)
            uiInput(it, event)
        }

        if (event.handled) {
            inputEventPool.free(event)
            return true
        }

        unhandledInputQueue += event
        return false
    }

    override fun onActionDown(inputType: InputType): Boolean {
        val event = inputEventPool.alloc().apply {
            type = InputEvent.Type.ACTION_DOWN
            this.inputType = inputType
        }

        // InputEvents go: input -> ui input -> unhandled input
        if (callInput(event)) {
            inputEventPool.free(event)
            return true
        }

        keyboardFocus?.let {
            it.callUiInput(event)
            uiInput(it, event)
            var handled = event.handled

            var next: Control? = null
            when (inputType) {
                uiInputSignals.uiFocusNext -> {
                    next = it.findNextValidFocus()
                    handled = true
                }

                uiInputSignals.uiFocusPrev -> {
                    next = it.findPreviousValidFocus()
                    handled = true
                }

                uiInputSignals.uiUp -> {
                    next = it.getFocusNeighbor(Control.Side.TOP)
                    handled = true
                }

                uiInputSignals.uiRight -> {
                    next = it.getFocusNeighbor(Control.Side.RIGHT)
                    handled = true
                }

                uiInputSignals.uiDown -> {
                    next = it.getFocusNeighbor(Control.Side.BOTTOM)
                    handled = true
                }

                uiInputSignals.uiLeft -> {
                    next = it.getFocusNeighbor(Control.Side.LEFT)
                    handled = true
                }

                else -> Unit
            }

            next?.grabFocus()
            if (handled) {
                inputEventPool.free(event)
                return true
            }
        }

        unhandledInputQueue += event
        return false
    }

    override fun onActionUp(inputType: InputType): Boolean {
        val event = inputEventPool.alloc().apply {
            type = InputEvent.Type.ACTION_UP
            this.inputType = inputType
        }

        // InputEvents go: input -> ui input -> unhandled input
        if (callInput(event)) {
            inputEventPool.free(event)
            return true
        }

        keyboardFocus?.let {
            it.callUiInput(event)
            uiInput(it, event)
            val handled = event.handled
            if (handled) {
                inputEventPool.free(event)
                return true
            }
        }

        unhandledInputQueue += event
        return false
    }

    override fun onActionRepeat(inputType: InputType): Boolean {
        val event = inputEventPool.alloc().apply {
            type = InputEvent.Type.ACTION_REPEAT
            this.inputType = inputType
        }

        // InputEvents go: input -> ui input -> unhandled input
        if (callInput(event)) {
            inputEventPool.free(event)
            return true
        }

        keyboardFocus?.let {
            it.callUiInput(event)
            uiInput(it, event)
            val handled = event.handled
            if (handled) {
                inputEventPool.free(event)
                return true
            }
        }
        unhandledInputQueue += event
        return false
    }

    override fun keyDown(key: Key): Boolean {
        if (controller.keyDown(key)) return true

        val event = inputEventPool.alloc().apply {
            type = InputEvent.Type.KEY_DOWN
            this.key = key
        }
        // InputEvents go: input -> ui input -> unhandled input
        if (callInput(event)) {
            inputEventPool.free(event)
            return true
        }

        keyboardFocus?.let {
            it.callUiInput(event)
            uiInput(it, event)
            val handled = event.handled
            if (handled) {
                inputEventPool.free(event)
                return true
            }
        }

        unhandledInputQueue += event
        return false
    }

    override fun keyUp(key: Key): Boolean {
        if (controller.keyUp(key)) return true

        val event = inputEventPool.alloc().apply {
            type = InputEvent.Type.KEY_UP
            this.key = key
        }

        // InputEvents go: input -> ui input -> unhandled input
        if (callInput(event)) {
            inputEventPool.free(event)
            return true
        }

        keyboardFocus?.let {
            it.callUiInput(event)
            uiInput(it, event)
            val handled = event.handled
            if (handled) {
                inputEventPool.free(event)
                return true
            }
        }
        unhandledInputQueue += event
        return false
    }

    override fun keyRepeat(key: Key): Boolean {
        if (controller.keyRepeat(key)) return true

        val event = inputEventPool.alloc().apply {
            type = InputEvent.Type.KEY_REPEAT
            this.key = key
        }
        // InputEvents go: input -> ui input -> unhandled input
        if (callInput(event)) {
            inputEventPool.free(event)
            return true
        }
        keyboardFocus?.let {
            it.callUiInput(event)
            uiInput(it, event)
            val handled = event.handled
            if (handled) {
                inputEventPool.free(event)
                return true
            }
        }
        unhandledInputQueue += event
        return false
    }

    override fun charTyped(character: Char): Boolean {
        if (controller.charTyped(character)) return true

        val event = inputEventPool.alloc().apply {
            type = InputEvent.Type.CHAR_TYPED
            char = character
        }
        // InputEvents go: input -> ui input -> unhandled input
        if (callInput(event)) {
            inputEventPool.free(event)
            return true
        }
        keyboardFocus?.let {
            it.callUiInput(event)
            uiInput(it, event)
            val handled = event.handled
            if (handled) {
                inputEventPool.free(event)
                return true
            }
        }
        unhandledInputQueue += event
        return false
    }

    override fun scrolled(amountX: Float, amountY: Float): Boolean {
        if (controller.scrolled(amountX, amountY)) return true

        val event = inputEventPool.alloc().apply {
            type = InputEvent.Type.SCROLLED
            scrollAmountX = amountX
            scrollAmountY = amountY
        }
        // InputEvents go: input -> ui input -> unhandled input
        if (callInput(event)) {
            inputEventPool.free(event)
            return true
        }
        mouseOverControl?.let {
            it.callUiInput(event)
            uiInput(it, event)
            val handled = event.handled
            if (handled) {
                inputEventPool.free(event)
                return true
            }
        }
        unhandledInputQueue += event
        return false
    }

    override fun gamepadButtonPressed(button: GameButton, pressure: Float, gamepad: Int): Boolean {
        return controller.gamepadButtonPressed(button, pressure, gamepad)
    }

    override fun gamepadButtonReleased(button: GameButton, gamepad: Int): Boolean {
        return controller.gamepadButtonReleased(button, gamepad)
    }

    override fun gamepadJoystickMoved(stick: GameStick, xAxis: Float, yAxis: Float, gamepad: Int): Boolean {
        return controller.gamepadJoystickMoved(stick, xAxis, yAxis, gamepad)
    }

    override fun gamepadTriggerChanged(button: GameButton, pressure: Float, gamepad: Int): Boolean {
        return controller.gamepadTriggerChanged(button, pressure, gamepad)
    }

    private fun fireEnterAndExit(overLast: Control?, screenX: Float, screenY: Float, pointer: Pointer): Control? {
        screenToSceneCoordinates(tempVec.set(screenX, screenY))

        val sceneX = tempVec.x
        val sceneY = tempVec.y
        val over = callHitTest(tempVec.x, tempVec.y)
        if (over == overLast) return overLast

        if (overLast != null) {
            val event = inputEventPool.alloc().apply {
                overLast.canvas?.screenToCanvasCoordinates(
                    tempVec.set(
                        screenX,
                        screenY
                    )
                )
                this.sceneX = sceneX
                this.sceneY = sceneY
                this.canvasX = tempVec.x
                this.canvasY = tempVec.y
                this.pointer = pointer
                overLast.toLocal(sceneX, sceneY, tempVec)
                localX = tempVec.x
                localY = tempVec.y
                type = InputEvent.Type.MOUSE_EXIT
            }
            overLast.let {
                it.callUiInput(event)
                uiInput(it, event)
            }
            inputEventPool.free(event)
        }

        if (over != null) {
            val event = inputEventPool.alloc().apply {
                over.canvas?.screenToCanvasCoordinates(
                    tempVec.set(
                        screenX,
                        screenY
                    )
                )
                this.sceneX = sceneX
                this.sceneY = sceneY
                this.canvasX = tempVec.x
                this.canvasY = tempVec.y
                this.pointer = pointer
                over.toLocal(sceneX, sceneY, tempVec)
                localX = tempVec.x
                localY = tempVec.y
                type = InputEvent.Type.MOUSE_ENTER
            }
            over.let {
                it.callUiInput(event)
                uiInput(it, event)
            }
            inputEventPool.free(event)
        }
        return over
    }


    private fun addTouchFocus(target: Control, pointer: Pointer) {
        touchFocusPool.alloc().apply {
            this.target = target
            this.pointer = pointer
        }.also { touchFocuses.add(it) }
    }

    private fun callHitTest(hx: Float, hy: Float): Control? {
        root.nodes.forEachReversed {
            val target = it.propagateHit(hx, hy)
            if (target != null) {
                return target
            }
        }
        return null
    }

    private fun callInput(event: InputEvent): Boolean {
        root.nodes.forEachReversed {
            if (it.propagateInput(event)) {
                return true
            }
        }
        return false
    }

    private fun callUnhandledInput(event: InputEvent): Boolean {
        root.nodes.forEachReversed {
            if (it.propagateUnhandledInput(event)) {
                return true
            }
        }
        return false
    }

    fun screenToSceneCoordinates(inOut: MutableVec2f) = sceneCanvas.screenToCanvasCoordinates(inOut)

    fun sceneToScreenCoordinates(inOut: MutableVec2f) = sceneCanvas.canvasToScreenCoordinates(inOut)

    private fun isInsideViewport(x: Int, y: Int): Boolean {
        val x0 = sceneCanvas.x
        val x1 = x0 + sceneCanvas.width
        val y0 = sceneCanvas.y
        val y1 = y0 + sceneCanvas.height
        val screenY = context.graphics.height - 1 - y
        return x in x0 until x1 && screenY in y0 until y1
    }

    /**
     * Adds the new to the [root].
     */
    operator fun plusAssign(node: Node) {
        root.addChild(node)
    }

    /**
     * Removes the node from the [root].
     */
    operator fun minusAssign(node: Node) {
        root.removeChild(node)
    }

    /**
     * Lifecycle method. Do any necessary unloading / disposing here. This is called when this scene is removed
     * from the active slot.
     */
    override fun dispose() {
        sceneCanvas.destroy()
        if (ownsBatch) {
            batch.dispose()
        }
        controller.removeInputMapProcessor(this)
        context.input.removeInputProcessor(this)
    }

    data class UiInputSignals(
        val uiAccept: InputType? = null,
        val uiSelect: InputType? = null,
        val uiCancel: InputType? = null,
        val uiFocusNext: InputType? = null,
        val uiFocusPrev: InputType? = null,
        val uiLeft: InputType? = null,
        val uiRight: InputType? = null,
        val uiUp: InputType? = null,
        val uiDown: InputType? = null,
        val uiHome: InputType? = null,
        val uiEnd: InputType? = null,
    )

    private data class TouchFocus(
        var target: Control? = null,
        var pointer: Pointer = Pointer.POINTER1,
    ) {
        fun reset() {
            target = null
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy