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

desktopMain.androidx.compose.ui.scene.WindowComposeSceneLayer.desktop.kt Maven / Gradle / Ivy

/*
 * Copyright 2023 The Android Open Source Project
 *
 * 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 androidx.compose.ui.scene

import org.jetbrains.skia.Rect as SkRect
import androidx.compose.runtime.CompositionContext
import androidx.compose.ui.awt.RenderSettings
import androidx.compose.ui.awt.getTransparentWindowBackground
import androidx.compose.ui.awt.setTransparent
import androidx.compose.ui.awt.toAwtRectangle
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.platform.PlatformWindowContext
import androidx.compose.ui.scene.skia.SkiaLayerComponent
import androidx.compose.ui.scene.skia.WindowSkiaLayerComponent
import androidx.compose.ui.skiko.OverlayRenderDecorator
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.roundToIntRect
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.window.density
import androidx.compose.ui.window.getDialogScrimBlendMode
import androidx.compose.ui.window.layoutDirectionFor
import androidx.compose.ui.window.sizeInPx
import java.awt.Point
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.JDialog
import javax.swing.JLayeredPane
import org.jetbrains.skia.Canvas
import org.jetbrains.skiko.SkiaLayerAnalytics

internal class WindowComposeSceneLayer(
    composeContainer: ComposeContainer,
    private val skiaLayerAnalytics: SkiaLayerAnalytics,
    private val transparent: Boolean,
    density: Density,
    layoutDirection: LayoutDirection,
    focusable: Boolean,
    compositionContext: CompositionContext,
    private val renderSettings: RenderSettings
) : DesktopComposeSceneLayer(composeContainer, density, layoutDirection) {
    private val window get() = requireNotNull(composeContainer.window)
    private val windowContext = PlatformWindowContext().also {
        it.isWindowTransparent = true
        it.setContainerSize(windowContainer.sizeInPx)
    }

    private val dialog = JDialog(
        window,
    ).also {
        it.isAlwaysOnTop = true
        it.isFocusable = focusable
        it.isUndecorated = true
        it.background = getTransparentWindowBackground(
            isWindowTransparent = transparent,
            renderApi = composeContainer.renderApi
        )
    }
    private val container = object : JLayeredPane() {
        override fun addNotify() {
            super.addNotify()
            mediator?.onComponentAttached()
        }
    }.also {
        it.layout = null
        it.setTransparent(transparent)

        dialog.contentPane = it
    }

    private val windowPositionListener = object : ComponentAdapter() {
        override fun componentMoved(e: ComponentEvent?) {
            onWindowContainerPositionChanged()
        }
    }

    override var mediator: ComposeSceneMediator? = null

    override var focusable: Boolean = focusable
        set(value) {
            field = value
            dialog.isFocusable = value
        }

    override var scrimColor: Color? = null

    init {
        val boundsInPx = windowContainer.sizeInPx.toRect()
        drawBounds = boundsInPx.roundToIntRect()
        mediator = ComposeSceneMediator(
            container = container,
            windowContext = windowContext,
            exceptionHandler = {
                composeContainer.exceptionHandler?.onException(it) ?: throw it
            },
            eventListener = eventListener,
            measureDrawLayerBounds = true,
            coroutineContext = compositionContext.effectCoroutineContext,
            skiaLayerComponentFactory = ::createSkiaLayerComponent,
            composeSceneFactory = ::createComposeScene,
        ).also {
            it.onWindowTransparencyChanged(true)
            it.sceneBoundsInPx = boundsInPx
            it.contentComponent.size = windowContainer.size
        }
        onUpdateBounds()

        dialog.isVisible = true

        // Track window position in addition to [onChangeWindowPosition] because [windowContainer]
        // might be not the same as real [window].
        window.addComponentListener(windowPositionListener)

        composeContainer.attachLayer(this)
    }

    override fun close() {
        super.close()
        composeContainer.detachLayer(this)
        mediator?.dispose()
        mediator = null

        window.removeComponentListener(windowPositionListener)

        dialog.dispose()
    }

    override fun onWindowContainerPositionChanged() {
        val scaledRectangle = drawBounds.toAwtRectangle(density)
        setDialogLocation(scaledRectangle.x, scaledRectangle.y)
    }

    override fun onWindowContainerSizeChanged() {
        windowContext.setContainerSize(windowContainer.sizeInPx)

        // Update compose constrains based on main window size
        mediator?.sceneBoundsInPx = Rect(
            offset = -drawBounds.topLeft.toOffset(),
            size = windowContainer.sizeInPx
        )
    }

    override fun onLayersChange() {
        // Force redraw because rendering depends on other layers
        // see [onRenderOverlay]
        dialog.repaint()
    }

    override fun onUpdateBounds() {
        val scaledRectangle = drawBounds.toAwtRectangle(density)
        setDialogLocation(scaledRectangle.x, scaledRectangle.y)
        dialog.setSize(scaledRectangle.width, scaledRectangle.height)
        mediator?.contentComponent?.setSize(scaledRectangle.width, scaledRectangle.height)
        mediator?.sceneBoundsInPx = Rect(
            offset = -drawBounds.topLeft.toOffset(),
            size = windowContainer.sizeInPx
        )
    }

    override fun onRenderOverlay(canvas: Canvas, width: Int, height: Int, transparent: Boolean) {
        val scrimColor = scrimColor ?: return
        val paint = Paint().apply {
            color = scrimColor
            blendMode = getDialogScrimBlendMode(transparent)
        }.asFrameworkPaint()
        canvas.drawRect(SkRect.makeWH(width.toFloat(), height.toFloat()), paint)
    }

    private fun createSkiaLayerComponent(mediator: ComposeSceneMediator): SkiaLayerComponent {
        val renderDelegate = OverlayRenderDecorator(
            recordDrawBounds(mediator)
        ) { canvas, width, height ->
            composeContainer.layersAbove(this).forEach {
                it.onRenderOverlay(canvas, width, height, transparent)
            }
        }
        return WindowSkiaLayerComponent(
            mediator = mediator,
            windowContext = windowContext,
            renderDelegate = renderDelegate,
            skiaLayerAnalytics = skiaLayerAnalytics,
            renderSettings = renderSettings,
        )
    }

    private fun createComposeScene(mediator: ComposeSceneMediator): ComposeScene {
        val density = container.density
        val layoutDirection = layoutDirectionFor(container)
        return PlatformLayersComposeScene(
            coroutineContext = mediator.coroutineContext,
            density = density,
            invalidate = mediator::onComposeInvalidation,
            layoutDirection = layoutDirection,
            composeSceneContext = composeContainer.createComposeSceneContext(
                platformContext = mediator.platformContext
            ),
        )
    }

    private fun setDialogLocation(x: Int, y: Int) {
        if (!windowContainer.isShowing) {
            return
        }
        val locationOnScreen = windowContainer.locationOnScreen
        dialog.location = Point(
            locationOnScreen.x + x,
            locationOnScreen.y + y
        )
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy