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 ...@@ -4,6 +4,7 @@ import android.content.Context
import android.view.* import android.view.*
import org.jetbrains.skia.Canvas import org.jetbrains.skia.Canvas
import org.jetbrains.skia.PixelGeometry import org.jetbrains.skia.PixelGeometry
import org.jetbrains.skia.Color
actual open class SkiaLayer { actual open class SkiaLayer {
private var glView: SkikoSurfaceView? = null private var glView: SkikoSurfaceView? = null
...@@ -25,6 +26,15 @@ actual open class SkiaLayer { ...@@ -25,6 +26,15 @@ actual open class SkiaLayer {
if (value) throw IllegalArgumentException("transparency unsupported") 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 var renderDelegate: SkikoRenderDelegate? = null
actual fun attachTo(container: Any) { actual fun attachTo(container: Any) {
......
...@@ -17,11 +17,11 @@ import java.awt.im.InputMethodRequests ...@@ -17,11 +17,11 @@ import java.awt.im.InputMethodRequests
import java.beans.PropertyChangeListener import java.beans.PropertyChangeListener
import java.util.concurrent.CancellationException import java.util.concurrent.CancellationException
import javax.accessibility.Accessible import javax.accessibility.Accessible
import javax.accessibility.AccessibleContext
import javax.accessibility.AccessibleRole
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
import javax.swing.SwingUtilities.isEventDispatchThread import javax.swing.SwingUtilities.isEventDispatchThread
import javax.swing.UIManager
import javax.swing.event.AncestorEvent import javax.swing.event.AncestorEvent
import javax.swing.event.AncestorListener import javax.swing.event.AncestorListener
import kotlin.math.floor import kotlin.math.floor
...@@ -32,7 +32,7 @@ actual open class SkiaLayer internal constructor( ...@@ -32,7 +32,7 @@ actual open class SkiaLayer internal constructor(
private val renderFactory: RenderFactory = RenderFactory.Default, private val renderFactory: RenderFactory = RenderFactory.Default,
private val analytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty, private val analytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty,
actual val pixelGeometry: PixelGeometry = PixelGeometry.UNKNOWN, actual val pixelGeometry: PixelGeometry = PixelGeometry.UNKNOWN,
) : JPanel() { ) : JComponent(), Accessible {
internal companion object { internal companion object {
init { init {
...@@ -45,18 +45,6 @@ actual open class SkiaLayer internal constructor( ...@@ -45,18 +45,6 @@ actual open class SkiaLayer internal constructor(
ContentScale, 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 internal val backedLayer: HardwareLayer
constructor( constructor(
...@@ -100,7 +88,6 @@ actual open class SkiaLayer internal constructor( ...@@ -100,7 +88,6 @@ actual open class SkiaLayer internal constructor(
private var latestReceivedGraphicsContextScaleTransform: AffineTransform? = null private var latestReceivedGraphicsContextScaleTransform: AffineTransform? = null
init { init {
isOpaque = false
layout = null layout = null
backedLayer = object : HardwareLayer(externalAccessibleFactory) { backedLayer = object : HardwareLayer(externalAccessibleFactory) {
override fun paint(g: Graphics) { override fun paint(g: Graphics) {
...@@ -206,6 +193,43 @@ actual open class SkiaLayer internal constructor( ...@@ -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 // Override to make final, because it's called it in the init block
final override fun addAncestorListener(listener: AncestorListener?) { final override fun addAncestorListener(listener: AncestorListener?) {
super.addAncestorListener(listener) super.addAncestorListener(listener)
...@@ -662,10 +686,24 @@ actual open class SkiaLayer internal constructor( ...@@ -662,10 +686,24 @@ actual open class SkiaLayer internal constructor(
fun requestNativeFocusOnAccessible(accessible: Accessible?) { fun requestNativeFocusOnAccessible(accessible: Accessible?) {
backedLayer.requestNativeFocusOnAccessible(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) { fun SkiaLayer.disableTitleBar(customHeaderHeight: Float) {
backedLayer.disableTitleBar(customHeaderHeight) backedLayer.disableTitleBar(customHeaderHeight)
......
...@@ -10,9 +10,9 @@ import java.awt.color.ColorSpace ...@@ -10,9 +10,9 @@ import java.awt.color.ColorSpace
import java.awt.image.* import java.awt.image.*
internal class SoftwareContextHandler(layer: SkiaLayer) : ContextFreeContextHandler(layer) { internal class SoftwareContextHandler(layer: SkiaLayer) : ContextFreeContextHandler(layer) {
override fun isTransparentBackground(): Boolean { override fun isTransparentBackgroundSupported(): Boolean {
// TODO: why Software rendering has another transparency logic from the begginning // TODO: why Software rendering has another transparency logic from the beginning
return hostOs == OS.MacOS && layer.transparency return hostOs == OS.MacOS
} }
val colorModel = ComponentColorModel( val colorModel = ComponentColorModel(
......
...@@ -1057,15 +1057,6 @@ class SkiaLayerTest { ...@@ -1057,15 +1057,6 @@ class SkiaLayerTest {
// a smaller black window on top of it while screenshotting the pixel at the center, // 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. // 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 bgColor = Color.GREEN
val fgColor = Color.BLACK val fgColor = Color.BLACK
val backgroundWindow = JFrame().also { val backgroundWindow = JFrame().also {
...@@ -1329,6 +1320,60 @@ class SkiaLayerTest { ...@@ -1329,6 +1320,60 @@ class SkiaLayerTest {
assertTrue(drawCalls > initDrawCalls) 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 class RectRenderer(
private val getContentScale: () -> Float, private val getContentScale: () -> Float,
var rectWidth: Int, var rectWidth: Int,
...@@ -1365,8 +1410,6 @@ class SkiaLayerTest { ...@@ -1365,8 +1410,6 @@ class SkiaLayerTest {
} }
} }
private class AnimatedBoxRenderer( private class AnimatedBoxRenderer(
private val layer: SkiaLayer, private val layer: SkiaLayer,
private val pixelsPerSecond: Double, private val pixelsPerSecond: Double,
...@@ -1424,6 +1467,16 @@ class SkiaLayerTest { ...@@ -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)) private fun JFrame.close() = dispatchEvent(WindowEvent(this, WindowEvent.WINDOW_CLOSING))
...@@ -33,6 +33,11 @@ expect open class SkiaLayer { ...@@ -33,6 +33,11 @@ expect open class SkiaLayer {
*/ */
var transparency: Boolean var transparency: Boolean
/**
* The color, in ARGB format, with which the layer is cleared before rendering.
*/
var backgroundColor: Int
/** /**
* Underlying platform component. * Underlying platform component.
*/ */
......
...@@ -2,7 +2,7 @@ package org.jetbrains.skiko ...@@ -2,7 +2,7 @@ package org.jetbrains.skiko
import org.jetbrains.skia.Canvas import org.jetbrains.skia.Canvas
interface SkikoRenderDelegate { fun interface SkikoRenderDelegate {
fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long)
} }
......
...@@ -41,22 +41,26 @@ internal abstract class ContextHandler( ...@@ -41,22 +41,26 @@ internal abstract class ContextHandler(
} }
initCanvas() initCanvas()
canvas?.apply { 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() drawContent()
} }
flush() flush()
} }
protected open fun isTransparentBackground(): Boolean { protected open fun isTransparentBackgroundSupported(): Boolean {
if (hostOs == OS.MacOS) { if (hostOs == OS.MacOS) {
// MacOS transparency is always supported // MacOS transparency is always supported
return true return true
} }
if (layer.fullscreen) {
// for non-MacOS in fullscreen transparency is not supported // for non-MacOS in fullscreen transparency is not supported
return false return !layer.fullscreen
}
// for non-MacOS in non-fullscreen transparency provided by [layer]
return layer.transparency
} }
} }
...@@ -15,6 +15,9 @@ actual open class SkiaLayer { ...@@ -15,6 +15,9 @@ actual open class SkiaLayer {
actual var transparency: Boolean actual var transparency: Boolean
get() = TODO("Not yet implemented") get() = TODO("Not yet implemented")
set(value) {} set(value) {}
actual var backgroundColor: Int
get() = TODO("Not yet implemented")
set(value) {}
actual val component: Any? actual val component: Any?
get() = TODO("Not yet implemented") get() = TODO("Not yet implemented")
actual fun needRender(throttledToVsync: Boolean) { actual fun needRender(throttledToVsync: Boolean) {
......
...@@ -58,6 +58,15 @@ actual open class SkiaLayer { ...@@ -58,6 +58,15 @@ actual open class SkiaLayer {
if (value) throw IllegalArgumentException("transparency unsupported") 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] * Underlying [NSView]
*/ */
......
...@@ -2,6 +2,7 @@ package org.jetbrains.skiko ...@@ -2,6 +2,7 @@ package org.jetbrains.skiko
import kotlinx.cinterop.useContents import kotlinx.cinterop.useContents
import org.jetbrains.skia.Canvas import org.jetbrains.skia.Canvas
import org.jetbrains.skia.Color
import org.jetbrains.skia.PixelGeometry import org.jetbrains.skia.PixelGeometry
import org.jetbrains.skia.Surface import org.jetbrains.skia.Surface
...@@ -23,6 +24,15 @@ actual open class SkiaLayer { ...@@ -23,6 +24,15 @@ actual open class SkiaLayer {
get() = false get() = false
set(_) { throw UnsupportedOperationException() } 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) { actual fun needRender(throttledToVsync: Boolean) {
needRedrawCallback.invoke() needRedrawCallback.invoke()
} }
......
package org.jetbrains.skiko package org.jetbrains.skiko
import org.jetbrains.skia.Canvas import org.jetbrains.skia.Canvas
import org.jetbrains.skia.Color
import org.jetbrains.skia.PixelGeometry import org.jetbrains.skia.PixelGeometry
import org.jetbrains.skiko.w3c.HTMLCanvasElement import org.jetbrains.skiko.w3c.HTMLCanvasElement
import org.jetbrains.skiko.w3c.window import org.jetbrains.skiko.w3c.window
...@@ -45,6 +46,15 @@ actual open class SkiaLayer { ...@@ -45,6 +46,15 @@ actual open class SkiaLayer {
if (value) throw Exception("Transparency is not supported!") 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. * 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