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

net.peanuuutz.fork.ui.preset.Panel.kt Maven / Gradle / Ivy

The newest version!
package net.peanuuutz.fork.ui.preset

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.currentCompositeKeyHash
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import net.peanuuutz.fork.render.screen.clip.ClipOp
import net.peanuuutz.fork.ui.foundation.draw.BorderStroke
import net.peanuuutz.fork.ui.foundation.draw.border
import net.peanuuutz.fork.ui.foundation.draw.clip
import net.peanuuutz.fork.ui.foundation.input.detect
import net.peanuuutz.fork.ui.foundation.layout.Box
import net.peanuuutz.fork.ui.foundation.layout.BoxScope
import net.peanuuutz.fork.ui.foundation.layout.PaddingValues
import net.peanuuutz.fork.ui.foundation.layout.minSize
import net.peanuuutz.fork.ui.foundation.layout.padding
import net.peanuuutz.fork.ui.inspection.InspectInfo
import net.peanuuutz.fork.ui.preset.theme.Theme
import net.peanuuutz.fork.ui.ui.draw.ContentDrawScope
import net.peanuuutz.fork.ui.ui.draw.DrawScope
import net.peanuuutz.fork.ui.ui.draw.Painter
import net.peanuuutz.fork.ui.ui.draw.inset
import net.peanuuutz.fork.ui.ui.draw.shape.Outline
import net.peanuuutz.fork.ui.ui.modifier.Modifier
import net.peanuuutz.fork.ui.ui.modifier.ModifierNodeElement
import net.peanuuutz.fork.ui.ui.modifier.input.pointerInput
import net.peanuuutz.fork.ui.ui.modifier.layout.onPlaced
import net.peanuuutz.fork.ui.ui.node.DrawModifierNode
import net.peanuuutz.fork.ui.ui.node.GlobalLayoutCallbackModifierNode
import net.peanuuutz.fork.ui.ui.node.LayoutInfo
import net.peanuuutz.fork.ui.ui.node.ModifierNode
import net.peanuuutz.fork.ui.ui.unit.IntSize
import net.peanuuutz.fork.util.common.Color
import net.peanuuutz.fork.util.common.fastForEach
import net.peanuuutz.fork.util.common.fastMap

@Composable
fun Panel(
    modifier: Modifier = Modifier,
    panelStyle: PanelStyle = Theme.panel,
    contentPadding: PaddingValues = PanelDefaults.ContentPadding,
    content: @Composable BoxScope.() -> Unit
) {
    val insets = remember { PanelInsets() }
    val border by panelStyle.border()
    val background by panelStyle.background()
    val inset by panelStyle.inset()

    Box(
        modifier = modifier
            .minSize(PanelDefaults.MinSize)
            .pointerInput {
                detect { it.consume() }
            }
            .border(border)
            .panelStructure(
                insets = insets,
                background = background,
                inset = inset
            )
            .padding(contentPadding)
            .clip()
    ) {
        val contentColor by panelStyle.content()

        CompositionLocalProvider(
            LocalContentColor provides contentColor,
            LocalPanelInsets provides insets
        ) {
            content()
        }
    }
}

@Composable
fun PanelInset(
    modifier: Modifier = Modifier,
    panelStyle: PanelStyle = Theme.panel,
    content: @Composable BoxScope.() -> Unit
) {
    val key = currentCompositeKeyHash
    val panelInsets by rememberUpdatedState(LocalPanelInsets.current)

    DisposableEffect(Unit) {
        onDispose {
            panelInsets?.remove(key)
        }
    }

    Box(
        modifier = modifier
            .minSize(PanelDefaults.InsetMinSize)
            .onPlaced { info ->
                panelInsets?.add(key, info)
            }
    ) {
        val contentColor by panelStyle.insetContent()

        CompositionLocalProvider(
            LocalContentColor provides contentColor
        ) {
            content()
        }
    }
}

object PanelDefaults {
    val MinSize: IntSize = IntSize(24, 24)

    val InsetMinSize: IntSize = IntSize(12, 12)

    val ContentPadding: PaddingValues = PaddingValues(8)
}

@Stable
interface PanelStyle {
    @Composable
    fun border(): State

    @Composable
    fun background(): State

    @Composable
    fun content(): State

    @Composable
    fun inset(): State

    @Composable
    fun insetContent(): State

    @Stable
    abstract class Delegated(
        val delegate: PanelStyle
    ) : PanelStyle by delegate
}

val Theme.panel: PanelStyle
    @ReadOnlyComposable
    @Composable
    get() = LocalPanel.current

@NonRestartableComposable
@Composable
fun PanelStyleProvider(
    panelStyle: PanelStyle,
    content: @Composable () -> Unit
) {
    CompositionLocalProvider(
        LocalPanel provides panelStyle,
        content = content
    )
}

@Immutable
class DefaultPanelStyle(
    val border: BorderStroke,
    val background: Painter,
    val content: Color,
    val inset: Painter,
    val insetContent: Color
) : PanelStyle {
    @Composable
    override fun border(): State {
        return rememberUpdatedState(border)
    }

    @Composable
    override fun background(): State {
        return rememberUpdatedState(background)
    }

    @Composable
    override fun content(): State {
        return rememberUpdatedState(content)
    }

    @Composable
    override fun inset(): State {
        return rememberUpdatedState(inset)
    }

    @Composable
    override fun insetContent(): State {
        return rememberUpdatedState(insetContent)
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is DefaultPanelStyle) return false
        if (border != other.border) return false
        if (background != other.background) return false
        if (content != other.content) return false
        if (inset != other.inset) return false
        return true
    }

    override fun hashCode(): Int {
        var result = border.hashCode()
        result = 31 * result + background.hashCode()
        result = 31 * result + content.hashCode()
        result = 31 * result + inset.hashCode()
        return result
    }

    override fun toString(): String {
        return "DefaultPanelStyle(border=$border, background=$background, " +
                "content=$content, inset=$inset)"
    }
}

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

private val LocalPanelInsets = compositionLocalOf { null }

private class PanelInsets : Iterable {
    private val delegate: MutableMap = mutableMapOf()

    var shouldRebuildCache: Boolean = false

    fun add(key: Int, info: LayoutInfo) {
        delegate[key] = info
        shouldRebuildCache = true
    }

    fun remove(key: Int) {
        delegate.remove(key)
        shouldRebuildCache = true
    }

    override fun iterator(): Iterator {
        return delegate.values.iterator()
    }
}

private fun Modifier.panelStructure(
    insets: PanelInsets,
    background: Painter,
    inset: Painter
): Modifier {
    val element = PanelStructureModifier(
        insets = insets,
        background = background,
        inset = inset
    )
    return this then element
}

private data class PanelStructureModifier(
    val insets: PanelInsets,
    val background: Painter,
    val inset: Painter
) : ModifierNodeElement() {
    override fun create(): PanelStructureModifierNode {
        return PanelStructureModifierNode(
            insets = insets,
            background = background,
            inset = inset
        )
    }

    override fun update(node: PanelStructureModifierNode) {
        node.background = background
        node.inset = inset
    }

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

private class PanelStructureModifierNode(
    val insets: PanelInsets,
    var background: Painter,
    var inset: Painter
) : ModifierNode(),
    DrawModifierNode,
    GlobalLayoutCallbackModifierNode
{
    private lateinit var panelInfo: LayoutInfo

    private var insetOutlines: List = emptyList()

    override fun ContentDrawScope.draw() {
        if (insets.shouldRebuildCache) {
            insets.shouldRebuildCache = false
            rebuildCache()
        }
        val insetOutlines = insetOutlines
        drawBackground(insetOutlines)
        drawContent()
        drawInsets(insetOutlines)
    }

    private fun rebuildCache() {
        val panelInfo = panelInfo
        insetOutlines = insets
            .map { panelInfo.localBoundsOf(it, clip = true) }
            .fastMap { Outline.Regular(it) }
    }

    private fun DrawScope.drawBackground(insetOutlines: List) {
        insetOutlines.fastForEach { outline ->
            canvas.saveWithClip(
                outline = outline,
                op = ClipOp.Xor
            )
        }
        with(background) {
            onDraw()
        }
        repeat(insetOutlines.size) { canvas.restore() }
    }

    private fun ContentDrawScope.drawInsets(insetOutlines: List) {
        insetOutlines.fastForEach { outline ->
            val rect = outline.rect
            inset(
                topLeft = rect.topLeft,
                size = rect.size
            ) {
                with(inset) {
                    onDraw()
                }
            }
        }
    }

    override fun onGloballyPlaced(info: LayoutInfo) {
        panelInfo = info
        insets.shouldRebuildCache = true
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy