Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow drawing outside of platform layers #1190

Merged
merged 21 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@

package androidx.compose.ui.awt

import java.awt.Component
import java.awt.Rectangle
import java.awt.event.KeyEvent
import java.awt.event.MouseEvent
import javax.swing.SwingUtilities

internal interface AwtEventListener {
/**
Expand Down Expand Up @@ -58,6 +61,37 @@ internal class AwtEventListeners(
}
}

internal class BoundsEventFilter(
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved
var bounds: Rectangle,
private val relativeTo: Component,
private val onOutside: (event: MouseEvent) -> Unit
) : AwtEventListener {
override fun onMouseEvent(event: MouseEvent): Boolean {
when (event.id) {
// Do not filter motion events
MouseEvent.MOUSE_MOVED,
MouseEvent.MOUSE_ENTERED,
MouseEvent.MOUSE_EXITED,
MouseEvent.MOUSE_DRAGGED -> return false
}
return if (event.isInBounds) {
false
} else {
onOutside(event)
true
}
}

private val MouseEvent.isInBounds: Boolean get() {
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved
val localPoint = if (component != relativeTo) {
SwingUtilities.convertPoint(component, point, relativeTo)
} else {
point
}
return bounds.contains(localPoint)
}
}

/**
* Filter out mouse events that report the primary button has changed state to pressed,
* but aren't themselves a mouse press event. This is needed because on macOS, AWT sends
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ internal class ComposeSceneMediator(
private var exceptionHandler: WindowExceptionHandler?,
private val eventListener: AwtEventListener = OnlyValidPrimaryMouseButtonFilter,

/**
* @see PlatformContext.measureDrawLayerBounds
*/
private val measureDrawLayerBounds: Boolean = false,

val coroutineContext: CoroutineContext,

skiaLayerComponentFactory: (ComposeSceneMediator) -> SkiaLayerComponent,
Expand Down Expand Up @@ -625,6 +630,7 @@ internal class ComposeSceneMediator(
override fun calculateLocalPosition(positionInWindow: Offset): Offset =
windowContext.calculateLocalPosition(container, positionInWindow)

override val measureDrawLayerBounds: Boolean = this@ComposeSceneMediator.measureDrawLayerBounds
override val viewConfiguration: ViewConfiguration = DesktopViewConfiguration()
override val textInputService: PlatformTextInputService = this@ComposeSceneMediator.textInputService

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalContext
import androidx.compose.ui.awt.AwtEventListener
import androidx.compose.ui.awt.AwtEventListeners
import androidx.compose.ui.awt.BoundsEventFilter
import androidx.compose.ui.awt.OnlyValidPrimaryMouseButtonFilter
import androidx.compose.ui.awt.toAwtRectangle
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.util.fastForEachReversed
import java.awt.Rectangle
import java.awt.event.KeyEvent
import java.awt.event.MouseEvent
import javax.swing.SwingUtilities
Expand All @@ -49,11 +52,22 @@ internal abstract class DesktopComposeSceneLayer(
protected val eventListener get() = AwtEventListeners(
OnlyValidPrimaryMouseButtonFilter,
DetectEventOutsideLayer(),
boundsEventFilter,
FocusableLayerEventFilter()
)
private val boundsEventFilter = BoundsEventFilter(
bounds = Rectangle(windowContainer.size),
relativeTo = windowContainer,
onOutside = ::onMouseEventOutside
)

protected abstract val mediator: ComposeSceneMediator?

/**
* Bounds of real drawings from previous render.
*/
protected var drawBounds = IntRect.Zero

private var outsidePointerCallback: ((eventType: PointerEventType) -> Unit)? = null
private var isClosed = false

Expand All @@ -69,6 +83,13 @@ internal abstract class DesktopComposeSceneLayer(
mediator?.onChangeLayoutDirection(value)
}

// It shouldn't be used for setting canvas size - it will crop drawings outside
override var boundsInWindow: IntRect = IntRect.Zero
set(value) {
field = value
boundsEventFilter.bounds = value.toAwtRectangle(density)
}

final override var compositionLocalContext: CompositionLocalContext?
get() = mediator?.compositionLocalContext
set(value) { mediator?.compositionLocalContext = value }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ package androidx.compose.ui.scene
import androidx.compose.runtime.CompositionContext
import androidx.compose.ui.awt.toAwtColor
import androidx.compose.ui.awt.toAwtRectangle
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.scene.skia.SkiaLayerComponent
import androidx.compose.ui.scene.skia.SwingSkiaLayerComponent
import androidx.compose.ui.skiko.RecordDrawRectSkikoViewDecorator
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.roundToIntRect
import androidx.compose.ui.window.density
import java.awt.Dimension
import java.awt.Graphics
Expand All @@ -52,8 +55,8 @@ internal class SwingComposeSceneLayer(
override fun addNotify() {
super.addNotify()
mediator?.onComponentAttached()
_boundsInWindow?.let {
mediator?.contentComponent?.bounds = it.toAwtRectangle(density)
if (!drawBounds.isEmpty) {
setComponentBounds(drawBounds)
}
}

Expand Down Expand Up @@ -82,7 +85,7 @@ internal class SwingComposeSceneLayer(
if (field.width != value.width || field.height != value.height) {
field = value
container.setBounds(0, 0, value.width, value.height)
if (_boundsInWindow == null) {
if (drawBounds.isEmpty) {
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved
mediator?.contentComponent?.size = container.size
}
mediator?.onChangeComponentSize()
Expand All @@ -97,18 +100,6 @@ internal class SwingComposeSceneLayer(
container.isFocusable = value
}

private var _boundsInWindow: IntRect? = null
override var boundsInWindow: IntRect
get() = _boundsInWindow ?: IntRect.Zero
set(value) {
_boundsInWindow = value
val localBounds = SwingUtilities.convertRectangle(
/* source = */ windowContainer,
/* aRectangle = */ value.toAwtRectangle(container.density),
/* destination = */ container)
mediator?.contentComponent?.bounds = localBounds
}

override var scrimColor: Color? = null
set(value) {
field = value
Expand All @@ -124,6 +115,7 @@ internal class SwingComposeSceneLayer(
composeContainer.exceptionHandler?.onException(it) ?: throw it
},
eventListener = eventListener,
measureDrawLayerBounds = true,
coroutineContext = compositionContext.effectCoroutineContext,
skiaLayerComponentFactory = ::createSkiaLayerComponent,
composeSceneFactory = ::createComposeScene,
Expand All @@ -149,10 +141,17 @@ internal class SwingComposeSceneLayer(
containerSize = IntSize(windowContainer.width, windowContainer.height)
}

private fun onDrawRectChange(rect: Rect) {
val bounds = rect.roundToIntRect().translate(drawBounds.topLeft)
drawBounds = bounds
setComponentBounds(bounds)
}

private fun createSkiaLayerComponent(mediator: ComposeSceneMediator): SkiaLayerComponent {
val skikoView = RecordDrawRectSkikoViewDecorator(mediator, ::onDrawRectChange)
return SwingSkiaLayerComponent(
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved
mediator = mediator,
skikoView = mediator,
skikoView = skikoView,
skiaLayerAnalytics = skiaLayerAnalytics
)
}
Expand All @@ -169,4 +168,13 @@ internal class SwingComposeSceneLayer(
),
)
}

private fun setComponentBounds(bounds: IntRect) {
val scaledRectangle = bounds.toAwtRectangle(density)
val localBounds = SwingUtilities.convertRectangle(
/* source = */ windowContainer,
/* aRectangle = */ scaledRectangle,
/* destination = */ container)
mediator?.contentComponent?.bounds = localBounds
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ 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.OverlaySkikoViewDecorator
import androidx.compose.ui.skiko.RecordDrawRectSkikoViewDecorator
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntRect
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
Expand Down Expand Up @@ -97,12 +99,6 @@ internal class WindowComposeSceneLayer(
dialog.isFocusable = value
}

override var boundsInWindow: IntRect = IntRect.Zero
set(value) {
field = value
setDialogBounds(value)
}

override var scrimColor: Color? = null

init {
Expand All @@ -113,6 +109,7 @@ internal class WindowComposeSceneLayer(
composeContainer.exceptionHandler?.onException(it) ?: throw it
},
eventListener = eventListener,
measureDrawLayerBounds = true,
coroutineContext = compositionContext.effectCoroutineContext,
skiaLayerComponentFactory = ::createSkiaLayerComponent,
composeSceneFactory = ::createComposeScene,
Expand Down Expand Up @@ -173,8 +170,16 @@ internal class WindowComposeSceneLayer(
canvas.drawRect(SkRect.makeWH(width.toFloat(), height.toFloat()), paint)
}

private fun onDrawRectChange(rect: Rect) {
val bounds = rect.roundToIntRect().translate(drawBounds.topLeft)
drawBounds = bounds
setDialogBounds(bounds)
}

private fun createSkiaLayerComponent(mediator: ComposeSceneMediator): SkiaLayerComponent {
val skikoView = OverlaySkikoViewDecorator(mediator) { canvas, width, height ->
val skikoView = OverlaySkikoViewDecorator(
RecordDrawRectSkikoViewDecorator(mediator, ::onDrawRectChange)
) { canvas, width, height ->
composeContainer.layersAbove(this).forEach {
it.onRenderOverlay(canvas, width, height, transparent)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,12 +364,13 @@ internal class RootNodeOwner(
drawBlock: (Canvas) -> Unit,
invalidateParentLayer: () -> Unit
) = RenderNodeLayer(
Snapshot.withoutReadObservation {
density = Snapshot.withoutReadObservation {
// density is a mutable state that is observed whenever layer is created. the layer
// is updated manually on draw, so not observing the density changes here helps with
// performance in layout.
density
},
measureDrawBounds = platformContext.measureDrawLayerBounds,
invalidateParentLayer = {
invalidateParentLayer()
snapshotInvalidationTracker.requestDraw()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import androidx.compose.ui.input.InputMode
import androidx.compose.ui.input.InputModeManager
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.node.OwnedLayer
import androidx.compose.ui.node.Owner
import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.scene.ComposeScene
Expand Down Expand Up @@ -76,6 +77,14 @@ interface PlatformContext {
fun calculateLocalPosition(positionInWindow: Offset): Offset =
positionInWindow

/**
* Determines if [OwnedLayer] should measure bounds for all drawings.
* It's required to determine bounds of any graphics even if it was drawn out of measured
* layout bounds (for example shadows). It might be used to resize platform views based on
* such bounds.
*/
val measureDrawLayerBounds: Boolean get() = false

val viewConfiguration: ViewConfiguration get() = EmptyViewConfiguration
val inputModeManager: InputModeManager
val textInputService: PlatformTextInputService get() = EmptyPlatformTextInputService
Expand Down
Loading
Loading