Skip to content

Commit

Permalink
Desktop window layer fixes (#1181)
Browse files Browse the repository at this point in the history
## Proposed Changes

- Draw scrim on the main view when it's required by separate window
layer
- Fix updating position of window layer - tracking the position of the
real window in addition to the `windowContainer`
- Fix propagation of `density`/`layoutDirection`
- Use focus-lost callback as click outside trigger

## Testing

Test: Set `compose.layers.type = WINDOW` and try to use `Popup`/`Dialog`
TODO: screenshot auto-testing
  • Loading branch information
MatkovIvan committed Mar 12, 2024
1 parent b6449b6 commit d6cd2d0
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.compose.ui.platform.PlatformWindowContext
import androidx.compose.ui.scene.skia.SkiaLayerComponent
import androidx.compose.ui.scene.skia.SwingSkiaLayerComponent
import androidx.compose.ui.scene.skia.WindowSkiaLayerComponent
import androidx.compose.ui.skiko.OverlaySkikoViewDecorator
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.util.fastForEach
Expand All @@ -47,6 +48,7 @@ import javax.swing.SwingUtilities
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineExceptionHandler
import org.jetbrains.skia.Canvas
import org.jetbrains.skiko.MainUIDispatcher
import org.jetbrains.skiko.SkiaLayerAnalytics

Expand Down Expand Up @@ -171,6 +173,7 @@ internal class ComposeContainer(
if (!container.isDisplayable) return

mediator.onChangeComponentPosition()
layers.fastForEach(DesktopComposeSceneLayer::onChangeWindowPosition)
}

private fun onChangeWindowSize() {
Expand All @@ -181,6 +184,13 @@ internal class ComposeContainer(
layers.fastForEach(DesktopComposeSceneLayer::onChangeWindowSize)
}

/**
* Callback to let layers draw overlay on main [mediator].
*/
private fun onRenderOverlay(canvas: Canvas, width: Int, height: Int) {
layers.fastForEach { it.onRenderOverlay(canvas, width, height) }
}

fun onChangeWindowTransparency(value: Boolean) {
windowContext.isWindowTransparent = value
mediator.onChangeWindowTransparency(value)
Expand Down Expand Up @@ -246,10 +256,15 @@ internal class ComposeContainer(
}

private fun createSkiaLayerComponent(mediator: ComposeSceneMediator): SkiaLayerComponent {
val skikoView = when (layerType) {
// Use overlay decorator to allow window layers draw scrim on the main window
LayerType.OnWindow -> OverlaySkikoViewDecorator(mediator, ::onRenderOverlay)
else -> mediator
}
return if (useSwingGraphics) {
SwingSkiaLayerComponent(mediator, skiaLayerAnalytics)
SwingSkiaLayerComponent(mediator, skikoView, skiaLayerAnalytics)
} else {
WindowSkiaLayerComponent(mediator, windowContext, skiaLayerAnalytics)
WindowSkiaLayerComponent(mediator, windowContext, skikoView, skiaLayerAnalytics)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.platform.a11y.AccessibilityController
import androidx.compose.ui.platform.a11y.ComposeSceneAccessible
import androidx.compose.ui.platform.DelegateRootForTestListener
import androidx.compose.ui.platform.DesktopTextInputService
import androidx.compose.ui.platform.EmptyViewConfiguration
Expand All @@ -43,6 +41,8 @@ import androidx.compose.ui.platform.PlatformContext
import androidx.compose.ui.platform.PlatformWindowContext
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
import androidx.compose.ui.platform.a11y.AccessibilityController
import androidx.compose.ui.platform.a11y.ComposeSceneAccessible
import androidx.compose.ui.scene.skia.SkiaLayerComponent
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.text.input.PlatformTextInputService
Expand Down Expand Up @@ -80,7 +80,6 @@ import org.jetbrains.skia.Canvas
import org.jetbrains.skiko.ClipRectangle
import org.jetbrains.skiko.ExperimentalSkikoApi
import org.jetbrains.skiko.GraphicsApi
import org.jetbrains.skiko.SkikoInput
import org.jetbrains.skiko.SkikoView
import org.jetbrains.skiko.hostOs
import org.jetbrains.skiko.swing.SkiaSwingLayer
Expand All @@ -103,12 +102,10 @@ internal class ComposeSceneMediator(

skiaLayerComponentFactory: (ComposeSceneMediator) -> SkiaLayerComponent,
composeSceneFactory: (ComposeSceneMediator) -> ComposeScene,
) {
) : SkikoView {
private var isDisposed = false
private val invisibleComponent = InvisibleComponent()

val skikoView: SkikoView = DesktopSkikoView()

private val semanticsOwnerListener = DesktopSemanticsOwnerListener()
var rootForTestListener: PlatformContext.RootForTestListener? by DelegateRootForTestListener()
val accessible: Accessible = ComposeSceneAccessible {
Expand Down Expand Up @@ -366,7 +363,6 @@ internal class ComposeSceneMediator(

// Decides which AWT events should be delivered, and which should be filtered out
private val awtEventFilter = object {

var isPrimaryButtonPressed = false

fun shouldSendMouseEvent(event: MouseEvent): Boolean {
Expand Down Expand Up @@ -463,7 +459,7 @@ internal class ComposeSceneMediator(
}

fun onComponentAttached() {
onChangeComponentDensity()
onChangeDensity()

_onComponentAttached?.invoke()
_onComponentAttached = null
Expand Down Expand Up @@ -526,9 +522,7 @@ internal class ComposeSceneMediator(
)
}

fun onChangeComponentDensity() = catchExceptions {
if (!container.isDisplayable) return
val density = container.density
fun onChangeDensity(density: Density = container.density) = catchExceptions {
if (scene.density != density) {
scene.density = density
onChangeComponentSize()
Expand All @@ -543,6 +537,27 @@ internal class ComposeSceneMediator(
scene.layoutDirection = layoutDirection
}

override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) = catchExceptions {
canvas.withSceneOffset {
scene.render(asComposeCanvas(), nanoTime)
}
}

private inline fun Canvas.withSceneOffset(block: Canvas.() -> Unit) {
// Offset of scene relative to [container]
val sceneBoundsOffset = sceneBoundsInPx?.topLeft ?: Offset.Zero
// Offset of canvas relative to [container]
val contentOffset = with(contentComponent) {
val scale = density.density
Offset(x * scale, y * scale)
}
val sceneOffset = sceneBoundsOffset - contentOffset
save()
translate(sceneOffset.x, sceneOffset.y)
block()
restore()
}

fun onRenderApiChanged(action: () -> Unit) {
skiaLayerComponent.onRenderApiChanged(action)
}
Expand All @@ -551,24 +566,6 @@ internal class ComposeSceneMediator(
keyboardModifiersRequireUpdate = true
}

private inner class DesktopSkikoView : SkikoView {
override val input: SkikoInput
get() = SkikoInput.Empty

override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
catchExceptions {
val composeCanvas = canvas.asComposeCanvas()
val offset = sceneBoundsInPx?.topLeft ?: Offset.Zero
val scale = contentComponent.density.density
val dx = contentComponent.x * scale - offset.x
val dy = contentComponent.y * scale - offset.y
composeCanvas.translate(-dx, -dy)
scene.render(composeCanvas, nanoTime)
composeCanvas.translate(dx, dy)
}
}
}

private inner class DesktopViewConfiguration : ViewConfiguration by EmptyViewConfiguration {
override val touchSlop: Float get() = with(platformComponent.density) { 18.dp.toPx() }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,41 @@

package androidx.compose.ui.scene

import org.jetbrains.skia.Canvas

/**
* Represents an abstract class for a desktop Compose scene layer.
*
* @see SwingComposeSceneLayer
* @see WindowComposeSceneLayer
*/
internal abstract class DesktopComposeSceneLayer : ComposeSceneLayer {

/**
* Called when the focus of the window containing main Compose view has changed.
*/
open fun onChangeWindowFocus() {
}

/**
* Called when position of the window containing main Compose view has changed.
*/
open fun onChangeWindowPosition() {
}

/**
* Called when size of the window containing main Compose view has changed.
*/
open fun onChangeWindowSize() {
}

/**
* Renders the overlay on the main Compose view canvas.
*
* @param canvas the canvas of the main Compose view
* @param width the width of the canvas
* @param height the height of the canvas
*/
open fun onRenderOverlay(canvas: Canvas, width: Int, height: Int) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,13 @@ internal class SwingComposeSceneLayer(
override var density: Density = density
set(value) {
field = value
// TODO: Pass it to mediator/scene
_mediator?.onChangeDensity(value)
}

override var layoutDirection: LayoutDirection = layoutDirection
set(value) {
field = value
// TODO: Pass it to mediator/scene
_mediator?.onChangeLayoutDirection(value)
}

override var focusable: Boolean = focusable
Expand Down Expand Up @@ -186,7 +186,11 @@ internal class SwingComposeSceneLayer(
}

private fun createSkiaLayerComponent(mediator: ComposeSceneMediator): SkiaLayerComponent {
return SwingSkiaLayerComponent(mediator, skiaLayerAnalytics)
return SwingSkiaLayerComponent(
mediator = mediator,
skikoView = mediator,
skiaLayerAnalytics = skiaLayerAnalytics
)
}

private fun createComposeScene(mediator: ComposeSceneMediator): ComposeScene {
Expand Down
Loading

0 comments on commit d6cd2d0

Please sign in to comment.