Unverified Commit 9c3169a5 authored by Alexander Maryanovsky's avatar Alexander Maryanovsky Committed by GitHub

Fix SkiaLayer background drawing (#1141)

parent e3e6430d
......@@ -4,6 +4,7 @@ import android.content.Context
import android.view.*
import org.jetbrains.skia.Canvas
import org.jetbrains.skia.PixelGeometry
import org.jetbrains.skia.Color
actual open class SkiaLayer {
private var glView: SkikoSurfaceView? = null
......@@ -25,6 +26,15 @@ actual open class SkiaLayer {
if (value) throw IllegalArgumentException("transparency unsupported")
}
/**
* The background color of the layer.
*/
actual var backgroundColor: Int = Color.WHITE
set(value) {
field = value
needRender()
}
actual var renderDelegate: SkikoRenderDelegate? = null
actual fun attachTo(container: Any) {
......
......@@ -17,11 +17,11 @@ import java.awt.im.InputMethodRequests
import java.beans.PropertyChangeListener
import java.util.concurrent.CancellationException
import javax.accessibility.Accessible
import javax.accessibility.AccessibleContext
import javax.accessibility.AccessibleRole
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.SwingUtilities
import javax.swing.SwingUtilities.isEventDispatchThread
import javax.swing.UIManager
import javax.swing.event.AncestorEvent
import javax.swing.event.AncestorListener
import kotlin.math.floor
......@@ -32,7 +32,7 @@ actual open class SkiaLayer internal constructor(
private val renderFactory: RenderFactory = RenderFactory.Default,
private val analytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty,
actual val pixelGeometry: PixelGeometry = PixelGeometry.UNKNOWN,
) : JPanel() {
) : JComponent(), Accessible {
internal companion object {
init {
......@@ -45,18 +45,6 @@ actual open class SkiaLayer internal constructor(
ContentScale,
}
private var _transparency: Boolean = false
actual var transparency: Boolean
get() = _transparency
set(value) {
_transparency = value
if (!value) {
background = UIManager.getColor("Panel.background")
} else {
background = Color(0, 0, 0, 0)
}
}
internal val backedLayer: HardwareLayer
constructor(
......@@ -100,7 +88,6 @@ actual open class SkiaLayer internal constructor(
private var latestReceivedGraphicsContextScaleTransform: AffineTransform? = null
init {
isOpaque = false
layout = null
backedLayer = object : HardwareLayer(externalAccessibleFactory) {
override fun paint(g: Graphics) {
......@@ -206,6 +193,43 @@ actual open class SkiaLayer internal constructor(
}
}
private var _transparency: Boolean = false
actual var transparency: Boolean
get() = _transparency
set(value) {
configureBackground(value, _background)
}
actual var backgroundColor: Int
get() = background.rgb // Will return an ancestor's non-null background after setBackground(null).
set(value) {
configureBackground(_transparency, Color(value, true))
}
// This is needed because after setBackground(null), getBackground() will not return null, but an ancestor's
// non-null background. But we need to preserve the null value when modifying `transparency`.
private var _background: Color? = null
override fun setBackground(bg: Color?) {
configureBackground(_transparency, bg)
}
private fun configureBackground(transparency: Boolean, bg: Color?) {
_transparency = transparency
_background = bg
// Note that SkiaLayer itself doesn't draw its background; only backedLayer does, as it's heavyweight.
// We set the property just so it can be read back correctly, and also for the case when bg==null, as that
// indicates the parent's background should be used (getBackground() calls parent.getBackground() if own
// background is null).
super.setBackground(bg)
// To enable transparency, the backedLayer's background must be transparent (also the window background).
backedLayer.background = if (transparency) Color(0, 0, 0, 0) else bg
needRender()
}
// Override to make final, because it's called it in the init block
final override fun addAncestorListener(listener: AncestorListener?) {
super.addAncestorListener(listener)
......@@ -662,10 +686,24 @@ actual open class SkiaLayer internal constructor(
fun requestNativeFocusOnAccessible(accessible: Accessible?) {
backedLayer.requestNativeFocusOnAccessible(accessible)
}
override fun getAccessibleContext(): AccessibleContext {
if (accessibleContext == null) {
accessibleContext = AccessibleSkiaLayer()
}
return accessibleContext
}
@Suppress("RedundantInnerClassModifier")
protected inner class AccessibleSkiaLayer : AccessibleJComponent() {
override fun getAccessibleRole(): AccessibleRole {
return AccessibleRole.PANEL
}
}
}
/**
* Disable showing window title bar.
* Disable showing the window title bar.
*/
fun SkiaLayer.disableTitleBar(customHeaderHeight: Float) {
backedLayer.disableTitleBar(customHeaderHeight)
......
......@@ -10,9 +10,9 @@ import java.awt.color.ColorSpace
import java.awt.image.*
internal class SoftwareContextHandler(layer: SkiaLayer) : ContextFreeContextHandler(layer) {
override fun isTransparentBackground(): Boolean {
// TODO: why Software rendering has another transparency logic from the begginning
return hostOs == OS.MacOS && layer.transparency
override fun isTransparentBackgroundSupported(): Boolean {
// TODO: why Software rendering has another transparency logic from the beginning
return hostOs == OS.MacOS
}
val colorModel = ComponentColorModel(
......
......@@ -1057,15 +1057,6 @@ class SkiaLayerTest {
// a smaller black window on top of it while screenshotting the pixel at the center,
// and making sure that pixel is always either black or green.
// We can't compare colors exactly because java.awt.Robot can return a slightly different color due to
// system color profile
fun Color.closeTo(other: Color): Boolean {
val diffLimit = 10
return (red - other.red).absoluteValue < diffLimit
&& (green - other.green).absoluteValue < diffLimit
&& (blue - other.blue).absoluteValue < diffLimit
}
val bgColor = Color.GREEN
val fgColor = Color.BLACK
val backgroundWindow = JFrame().also {
......@@ -1329,6 +1320,60 @@ class SkiaLayerTest {
assertTrue(drawCalls > initDrawCalls)
}
private suspend fun UiTestScope.testLayerBackground(
initLayer: UiTestWindow.() -> Unit = {}
) {
val window = UiTestWindow()
try {
window.setLocation(200, 200)
window.setSize(300, 300)
val layer = window.layer
layer.renderDelegate = SkikoRenderDelegate { _, _, _, _ -> }
initLayer(window)
layer.background = Color.RED
window.isVisible = true
delay(1000)
val robot = Robot()
val windowBounds = window.bounds
fun assertLayerIs(color: Color) {
val pixel = robot.getPixelColor(windowBounds.centerX.toInt(), windowBounds.centerY.toInt())
assertTrue(pixel.closeTo(color), "Actual pixel $pixel not close to expected $color")
}
assertLayerIs(Color.RED)
layer.background = Color.BLUE
delay(100)
assertLayerIs(Color.BLUE)
layer.background = Color.GREEN
delay(100)
assertLayerIs(Color.GREEN)
} finally {
window.dispose()
}
}
@Test
fun `layer background is drawn correctly`() = uiTest {
testLayerBackground()
}
@Test
fun `layer background is drawn correctly with transparency`() = uiTest {
if (renderApi == GraphicsApi.ANGLE) return@uiTest // See https://youtrack.jetbrains.com/issue/SKIKO-1089
testLayerBackground {
layer.transparency = true
layer.background = Color(0, 0, 0, 0)
isUndecorated = true
val transparentWindowHack = (hostOs == OS.Windows) && (renderApi != GraphicsApi.DIRECT3D)
background = if (transparentWindowHack) null else Color(0, 0, 0, 0)
}
}
private class RectRenderer(
private val getContentScale: () -> Float,
var rectWidth: Int,
......@@ -1365,8 +1410,6 @@ class SkiaLayerTest {
}
}
private class AnimatedBoxRenderer(
private val layer: SkiaLayer,
private val pixelsPerSecond: Double,
......@@ -1424,6 +1467,16 @@ class SkiaLayerTest {
}
}
/**
* Compares two colors, within a certain tolerance.
* We can't compare colors exactly because java.awt.Robot can return a slightly different color due to
* system color profile
*/
fun Color.closeTo(other: Color, diffLimit: Int = 10): Boolean {
return (red - other.red).absoluteValue < diffLimit
&& (green - other.green).absoluteValue < diffLimit
&& (blue - other.blue).absoluteValue < diffLimit
}
}
private fun JFrame.close() = dispatchEvent(WindowEvent(this, WindowEvent.WINDOW_CLOSING))
......@@ -33,6 +33,11 @@ expect open class SkiaLayer {
*/
var transparency: Boolean
/**
* The color, in ARGB format, with which the layer is cleared before rendering.
*/
var backgroundColor: Int
/**
* Underlying platform component.
*/
......
......@@ -2,7 +2,7 @@ package org.jetbrains.skiko
import org.jetbrains.skia.Canvas
interface SkikoRenderDelegate {
fun interface SkikoRenderDelegate {
fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long)
}
......
......@@ -41,22 +41,26 @@ internal abstract class ContextHandler(
}
initCanvas()
canvas?.apply {
clear(if (isTransparentBackground()) Color.TRANSPARENT else Color.WHITE)
val layerBg = layer.backgroundColor
clear(
if (layer.transparency && isTransparentBackgroundSupported()) {
layerBg
} else {
layerBg or 0xFF000000.toInt()
}
)
drawContent()
}
flush()
}
protected open fun isTransparentBackground(): Boolean {
protected open fun isTransparentBackgroundSupported(): Boolean {
if (hostOs == OS.MacOS) {
// MacOS transparency is always supported
return true
}
if (layer.fullscreen) {
// for non-MacOS in fullscreen transparency is not supported
return false
}
// for non-MacOS in non-fullscreen transparency provided by [layer]
return layer.transparency
return !layer.fullscreen
}
}
......@@ -15,6 +15,9 @@ actual open class SkiaLayer {
actual var transparency: Boolean
get() = TODO("Not yet implemented")
set(value) {}
actual var backgroundColor: Int
get() = TODO("Not yet implemented")
set(value) {}
actual val component: Any?
get() = TODO("Not yet implemented")
actual fun needRender(throttledToVsync: Boolean) {
......
......@@ -58,6 +58,15 @@ actual open class SkiaLayer {
if (value) throw IllegalArgumentException("transparency unsupported")
}
/**
* The background color of the layer.
*/
actual var backgroundColor: Int = Color.WHITE
set(value) {
field = value
needRender()
}
/**
* Underlying [NSView]
*/
......
......@@ -2,6 +2,7 @@ package org.jetbrains.skiko
import kotlinx.cinterop.useContents
import org.jetbrains.skia.Canvas
import org.jetbrains.skia.Color
import org.jetbrains.skia.PixelGeometry
import org.jetbrains.skia.Surface
......@@ -23,6 +24,15 @@ actual open class SkiaLayer {
get() = false
set(_) { throw UnsupportedOperationException() }
/**
* The background color of the layer, as transparency is not supported.
*/
actual var backgroundColor: Int = Color.WHITE
set(value) {
field = value
needRender()
}
actual fun needRender(throttledToVsync: Boolean) {
needRedrawCallback.invoke()
}
......
package org.jetbrains.skiko
import org.jetbrains.skia.Canvas
import org.jetbrains.skia.Color
import org.jetbrains.skia.PixelGeometry
import org.jetbrains.skiko.w3c.HTMLCanvasElement
import org.jetbrains.skiko.w3c.window
......@@ -45,6 +46,15 @@ actual open class SkiaLayer {
if (value) throw Exception("Transparency is not supported!")
}
/**
* The background color of the layer, as transparency is not supported.
*/
actual var backgroundColor: Int = Color.WHITE
set(value) {
field = value
needRender()
}
/**
* Schedules a drawFrame to the appropriate moment.
*/
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment