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

net.peanuuutz.fork.ui.foundation.input.Pressable.kt Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2020 The Android Open Source Project
 * Modifications Copyright 2022 Peanuuutz
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.peanuuutz.fork.ui.foundation.input

import androidx.compose.runtime.Stable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import net.peanuuutz.fork.ui.foundation.input.interaction.MutableInteractionSource
import net.peanuuutz.fork.ui.foundation.input.interaction.PressInteraction
import net.peanuuutz.fork.ui.foundation.input.interaction.detectAndEmitPressInteractions
import net.peanuuutz.fork.ui.foundation.input.interaction.emitCancelOnPress
import net.peanuuutz.fork.ui.foundation.input.interaction.emitPress
import net.peanuuutz.fork.ui.foundation.input.interaction.emitRelease
import net.peanuuutz.fork.ui.foundation.input.interaction.tryEmitCancelOnPress
import net.peanuuutz.fork.ui.inspection.InspectInfo
import net.peanuuutz.fork.ui.ui.context.key.Key
import net.peanuuutz.fork.ui.ui.context.key.KeyEvent
import net.peanuuutz.fork.ui.ui.context.pointer.PointerEvent
import net.peanuuutz.fork.ui.ui.modifier.Modifier
import net.peanuuutz.fork.ui.ui.modifier.ModifierNodeElement
import net.peanuuutz.fork.ui.ui.modifier.input.SuspendingPointerInputModifierNode
import net.peanuuutz.fork.ui.ui.node.BranchingModifierNode
import net.peanuuutz.fork.ui.ui.node.KeyEventPass
import net.peanuuutz.fork.ui.ui.node.KeyEventPass.Out
import net.peanuuutz.fork.ui.ui.node.KeyInputModifierNode
import net.peanuuutz.fork.ui.ui.node.PointerEventPass
import net.peanuuutz.fork.ui.ui.node.PointerInputModifierNode

@Stable
fun Modifier.pressable(
    interactionSource: MutableInteractionSource? = null,
    label: Any? = null,
    isEnabled: Boolean = true,
    onPress: (CoroutineScope.() -> Unit)? = null,
    onRelease: (CoroutineScope.() -> Unit)? = null
): Modifier {
    if (!isEnabled) {
        return this
    }
    val element = PressableModifier(
        interactionSource = interactionSource,
        label = label,
        onPress = onPress,
        onRelease = onRelease
    )
    return this then element
}

// ======== Internal ========

private data class PressableModifier(
    val interactionSource: MutableInteractionSource?,
    val label: Any?,
    val onPress: (CoroutineScope.() -> Unit)?,
    val onRelease: (CoroutineScope.() -> Unit)?
) : ModifierNodeElement() {
    override fun create(): PressableModifierNode {
        return PressableModifierNode(
            interactionSource = interactionSource,
            label = label,
            onPress = onPress,
            onRelease = onRelease
        )
    }

    override fun update(node: PressableModifierNode) {
        node.interactionSource = interactionSource
        node.label = label
        node.onPress = onPress
        node.onRelease = onRelease
    }

    override fun InspectInfo.inspect() {
        set("label", label)
    }
}

private class PressableModifierNode(
    interactionSource: MutableInteractionSource?,
    label: Any?,
    var onPress: (CoroutineScope.() -> Unit)?,
    var onRelease: (CoroutineScope.() -> Unit)?
) : BranchingModifierNode(),
    PointerInputModifierNode,
    KeyInputModifierNode
{
    var interactionSource: MutableInteractionSource? = interactionSource
        set(value) {
            if (field == value) {
                return
            }
            field?.tryEmitCancelOnPress(
                stateProvider = this::pressState,
                stateUpdater = this::pressState::set
            )
            field = value
        }

    var label: Any? = label
        set(value) {
            if (field == value) {
                return
            }
            interactionSource?.tryEmitCancelOnPress(
                stateProvider = this::pressState,
                stateUpdater = this::pressState::set
            )
            field = value
        }

    private val pointerInputHandler: SuspendingPointerInputModifierNode = branch {
        SuspendingPointerInputModifierNode {
            coroutineScope {
                launch {
                    detectAndEmitPressInteractions(
                        interactionSourceProvider = this@PressableModifierNode::interactionSource,
                        stateProvider = this@PressableModifierNode::pressState,
                        stateUpdater = this@PressableModifierNode::pressState::set,
                        labelProvider = this@PressableModifierNode::label
                    )
                }
                launch {
                    detectPressRelease(
                        onPress = {
                            val onPress = onPress
                            if (onPress != null) {
                                onPress()
                            }
                        },
                        onRelease = {
                            val onRelease = onRelease
                            if (onRelease != null) {
                                onRelease()
                            }
                        }
                    )
                }
            }
        }
    }

    private var pressState: PressInteraction.Press? = null

    override fun onDetach() {
        interactionSource?.tryEmitCancelOnPress(
            stateProvider = this::pressState,
            stateUpdater = this::pressState::set
        )
    }

    override fun onPointerEvent(pass: PointerEventPass, pointerEvent: PointerEvent) {
        pointerInputHandler.onPointerEvent(pass, pointerEvent)
    }

    override fun onKeyEvent(pass: KeyEventPass, keyEvent: KeyEvent): Boolean {
        if (pass != Out || keyEvent.key != Key.Enter) {
            return false
        }
        if (keyEvent.isPressed) {
            nodeScope.launch {
                val effectInteractionSource = interactionSource ?: return@launch
                effectInteractionSource.emitCancelOnPress(
                    stateProvider = this@PressableModifierNode::pressState,
                    stateUpdater = this@PressableModifierNode::pressState::set
                )
                effectInteractionSource.emitPress(
                    stateProvider = this@PressableModifierNode::pressState,
                    stateUpdater = this@PressableModifierNode::pressState::set,
                    labelProvider = this@PressableModifierNode::label
                )
            }
            val onPress = onPress
            if (onPress != null) {
                onPress(nodeScope)
            }
        } else {
            nodeScope.launch {
                interactionSource?.emitRelease(
                    stateProvider = this@PressableModifierNode::pressState,
                    stateUpdater = this@PressableModifierNode::pressState::set
                )
            }
            val onRelease = onRelease
            if (onRelease != null) {
                onRelease(nodeScope)
            }
        }
        return true
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy