Unverified Commit 163aec11 authored by Elijah Semyonov's avatar Elijah Semyonov Committed by GitHub

Refactoring pass for iOS counterpart of Skiko (#781)

* Refactor SkikoUIView and Metal interaction

* Move load logic to init

* Remove dead code

* Use contentScaleFactor for calculating drawable size

* Move UITouch.isPressed to filescope

* Add trace logs for anomaly situations
parent ce227e6b
......@@ -3,45 +3,30 @@ package org.jetbrains.skiko
import kotlinx.cinterop.useContents
import org.jetbrains.skia.Canvas
import org.jetbrains.skia.PixelGeometry
import org.jetbrains.skiko.context.MetalContextHandler
import org.jetbrains.skiko.redrawer.MetalRedrawer
import org.jetbrains.skia.Surface
import platform.UIKit.*
import kotlin.system.getTimeNanos
import org.jetbrains.skia.*
actual open class SkiaLayer {
fun isShowing(): Boolean {
return true
}
fun showScreenKeyboard() {
view?.becomeFirstResponder()
}
fun hideScreenKeyboard() { view?.resignFirstResponder() }
fun isScreenKeyboardOpen(): Boolean {
return if (view == null) false else view!!.isFirstResponder
}
internal var needRedrawCallback: () -> Unit = {}
actual var renderApi: GraphicsApi
get() = GraphicsApi.METAL
set(value) { throw UnsupportedOperationException() }
set(_) { throw UnsupportedOperationException() }
actual val contentScale: Float
get() = view!!.contentScaleFactor.toFloat()
actual var fullscreen: Boolean
get() = true
set(value) { throw UnsupportedOperationException() }
set(_) { throw UnsupportedOperationException() }
actual var transparency: Boolean
get() = false
set(value) { throw UnsupportedOperationException() }
set(_) { throw UnsupportedOperationException() }
actual fun needRedraw() {
redrawer?.needRedraw()
needRedrawCallback.invoke()
}
actual val component: Any?
......@@ -57,56 +42,24 @@ actual open class SkiaLayer {
return@useContents size.height.toFloat()
}
internal var view: UIView? = null
// We need to keep reference to gesturesDetector as Objective-C will only keep weak reference here.
internal var gesturesDetector = SkikoGesturesDetector(this)
var gesturesToListen: Array<SkikoGestureEventKind>? = null
set(value) {
field = value
initGestures()
}
internal fun initGestures() {
gesturesDetector.setGesturesToListen(gesturesToListen)
}
internal var view: SkikoUIView? = null
actual fun attachTo(container: Any) {
attachTo(container as UIView)
}
fun attachTo(view: UIView) {
this.view = view
contextHandler = MetalContextHandler(this)
// TODO: maybe add observer for view.viewDidDisappear() to detach us?
redrawer = MetalRedrawer(this).apply {
needRedraw()
}
view = container as SkikoUIView
}
private var isDisposed = false
actual fun detach() {
if (!isDisposed) {
redrawer?.dispose()
redrawer = null
contextHandler?.dispose()
contextHandler = null
isDisposed = true
}
view?.detach()
}
actual var skikoView: SkikoView? = null
internal var redrawer: MetalRedrawer? = null
private var contextHandler: MetalContextHandler? = null
actual var skikoView: SkikoView? = null
internal actual fun draw(canvas: Canvas) {
check(!isDisposed) { "SkiaLayer is disposed" }
val (w, h) = view!!.frame.useContents {
size.width to size.height
throw UnsupportedOperationException("Don't call it, artifact of wrong abstraction")
}
val pictureWidth = (w.toFloat() * contentScale).coerceAtLeast(0.0F)
val pictureHeight = (h.toFloat() * contentScale).coerceAtLeast(0.0F)
skikoView?.onRender(canvas, pictureWidth.toInt(), pictureHeight.toInt(), getTimeNanos())
internal fun draw(surface: Surface) {
skikoView?.onRender(surface.canvas, surface.width, surface.height, getTimeNanos())
}
actual val pixelGeometry: PixelGeometry
......
package org.jetbrains.skiko
import kotlinx.cinterop.ObjCAction
import kotlinx.cinterop.useContents
import platform.darwin.NSObject
import platform.Foundation.NSSelectorFromString
import platform.UIKit.*
// See https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/using_responders_and_the_responder_chain_to_handle_events?language=objc
internal class SkikoGesturesDetector(
private val layer: SkiaLayer
) : NSObject() {
val view: UIView?
get() = layer.view
@ObjCAction
fun onTap(sender: UITapGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.TAP,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onDoubleTap(sender: UITapGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.DOUBLETAP,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onLongPress(sender: UILongPressGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.LONGPRESS,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onPinch(sender: UIPinchGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.PINCH,
scale = sender.scale,
velocity = sender.velocity,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onRotation(sender: UIRotationGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.ROTATION,
rotation = sender.rotation,
velocity = sender.velocity,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onSwipe(sender: UISwipeGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.SWIPE,
direction = toSkikoGestureDirection(sender.direction),
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onPan(sender: UIPanGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.PAN,
state = toSkikoGestureState(sender.state)
)
)
}
// We have ':' in selector to take care of function argument.
private val gestureRecognizers = hashMapOf(
SkikoGestureEventKind.TAP to UITapGestureRecognizer(this, NSSelectorFromString("onTap:")),
SkikoGestureEventKind.DOUBLETAP to UITapGestureRecognizer(this, NSSelectorFromString("onDoubleTap:")).apply {
numberOfTapsRequired = 2.toULong()
},
SkikoGestureEventKind.LONGPRESS to UILongPressGestureRecognizer(this, NSSelectorFromString("onLongPress:")),
SkikoGestureEventKind.PINCH to UIPinchGestureRecognizer(this, NSSelectorFromString("onPinch:")),
SkikoGestureEventKind.ROTATION to UIRotationGestureRecognizer(this, NSSelectorFromString("onRotation:")),
SkikoGestureEventKind.SWIPE to UISwipeGestureRecognizer(this, NSSelectorFromString("onSwipe:")),
SkikoGestureEventKind.PAN to UIPanGestureRecognizer(this, NSSelectorFromString("onPan:"))
)
fun setGesturesToListen(gestures: Array<SkikoGestureEventKind>?) {
clearGesturesToListen()
if (!gestures.isNullOrEmpty()) {
for (gesture in gestures) {
if (gestureRecognizers.containsKey(gesture)) {
view?.addGestureRecognizer(gestureRecognizers.get(gesture)!!)
}
}
}
}
private fun clearGesturesToListen() {
for ((key, value) in gestureRecognizers) {
view?.removeGestureRecognizer(value)
}
}
}
......@@ -4,8 +4,13 @@ import kotlinx.cinterop.*
import org.jetbrains.skia.Point
import org.jetbrains.skia.Rect
import org.jetbrains.skiko.ios.SkikoUITextInputTraits
import org.jetbrains.skiko.redrawer.MetalRedrawer
import platform.CoreGraphics.*
import platform.Foundation.*
import platform.Metal.MTLCreateSystemDefaultDevice
import platform.Metal.MTLDeviceProtocol
import platform.Metal.MTLPixelFormatBGRA8Unorm
import platform.QuartzCore.CAMetalLayer
import platform.UIKit.*
import platform.darwin.NSInteger
import kotlin.math.max
......@@ -18,40 +23,83 @@ import kotlin.math.min
@Suppress("CONFLICTING_OVERLOADS")
@ExportObjCClass
class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
@OverrideInit
constructor(frame: CValue<CGRect>) : super(frame)
companion object : UIViewMeta() {
override fun layerClass() = CAMetalLayer
}
@Suppress("UNUSED") // required by Objective-C runtime
@OverrideInit
constructor(coder: NSCoder) : super(coder)
init {
multipleTouchEnabled = true
constructor(coder: NSCoder) : super(coder) {
throw UnsupportedOperationException("init(coder: NSCoder) is not supported for SkikoUIView")
}
private var skiaLayer: SkiaLayer? = null
private val _device: MTLDeviceProtocol =
MTLCreateSystemDefaultDevice() ?: throw IllegalStateException("Metal is not supported on this system")
private val _metalLayer: CAMetalLayer get() = layer as CAMetalLayer
private var _skiaLayer: SkiaLayer? = null
private var _pointInside: (Point, UIEvent?) -> Boolean = { _, _ -> true }
private var _skikoUITextInputTrains: SkikoUITextInputTraits = object : SkikoUITextInputTraits {}
private var _inputDelegate: UITextInputDelegateProtocol? = null
private var _currentTextMenuActions: TextActions? = null
private lateinit var _redrawer: MetalRedrawer
/*
* When there at least one tracked touch, we need notify redrawer about it. It should schedule CADisplayLink which
* affects frequency of polling UITouch events on high frequency display and forces it to match display refresh rate.
*/
private var _touchesCount = 0
set(value) {
field = value
val needHighFrequencyPolling = value > 0
_redrawer.needsProactiveDisplayLink = needHighFrequencyPolling
}
init {
multipleTouchEnabled = true
opaque = false // For UIKit interop through a "Hole"
_metalLayer.also {
// Workaround for KN compiler bug
// Type mismatch: inferred type is platform.Metal.MTLDeviceProtocol but objcnames.protocols.MTLDeviceProtocol? was expected
@Suppress("USELESS_CAST")
it.device = _device as objcnames.protocols.MTLDeviceProtocol?
it.pixelFormat = MTLPixelFormatBGRA8Unorm
doubleArrayOf(0.0, 0.0, 0.0, 0.0).usePinned { pinned ->
it.backgroundColor = CGColorCreate(CGColorSpaceCreateDeviceRGB(), pinned.addressOf(0))
}
it.framebufferOnly = false
}
}
// merging two constructors might cause a binary incompatibility, which will result in a unclear linking error,
// if a project using newer compose depends on an older compose transitively
// https://youtrack.jetbrains.com/issue/KT-60399
@Suppress("UNUSED") // public API
constructor(
skiaLayer: SkiaLayer,
frame: CValue<CGRect> = CGRectNull.readValue(),
pointInside: (Point, UIEvent?) -> Boolean = {_,_-> true }
pointInside: (Point, UIEvent?) -> Boolean = { _, _ -> true }
) : this(skiaLayer, frame, pointInside, skikoUITextInputTrains = object : SkikoUITextInputTraits {})
constructor(
skiaLayer: SkiaLayer,
frame: CValue<CGRect> = CGRectNull.readValue(),
pointInside: (Point, UIEvent?) -> Boolean = {_,_-> true },
pointInside: (Point, UIEvent?) -> Boolean = { _, _ -> true },
skikoUITextInputTrains: SkikoUITextInputTraits
) : super(frame) {
this.skiaLayer = skiaLayer
_skiaLayer = skiaLayer
_pointInside = pointInside
_skikoUITextInputTrains = skikoUITextInputTrains
_redrawer = MetalRedrawer(_metalLayer) { surface ->
skiaLayer.draw(surface)
}
skiaLayer.needRedrawCallback = _redrawer::needRedraw
skiaLayer.view = this
}
/**
......@@ -88,7 +136,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
menu.hideMenu()
}
fun isTextMenuShown():Boolean {
fun isTextMenuShown(): Boolean {
return _currentTextMenuActions != null
}
......@@ -108,20 +156,32 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
_currentTextMenuActions?.selectAll?.invoke()
}
fun detach() = skiaLayer?.detach()
internal fun detach() {
_redrawer.dispose()
}
fun load(): SkikoUIView {
val (width, height) = UIScreen.mainScreen.bounds.useContents {
this.size.width to this.size.height
// TODO: redundant, remove in next refactor pass
return this
}
override fun didMoveToWindow() {
super.didMoveToWindow()
window?.screen?.let {
contentScaleFactor = it.scale
_redrawer.maximumFramesPerSecond = it.maximumFramesPerSecond
}
setFrame(CGRectMake(0.0, 0.0, width, height))
contentScaleFactor = UIScreen.mainScreen.scale
skiaLayer?.let { layer ->
layer.attachTo(this)
layer.initGestures()
}
return this
override fun layoutSubviews() {
super.layoutSubviews()
val scaledSize = bounds.useContents {
CGSizeMake(size.width * contentScaleFactor, size.height * contentScaleFactor)
}
_metalLayer.drawableSize = scaledSize
}
fun showScreenKeyboard() = becomeFirstResponder()
......@@ -133,7 +193,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* https://developer.apple.com/documentation/uikit/uikeyinput/1614457-hastext
*/
override fun hasText(): Boolean {
return skiaLayer?.skikoView?.input?.hasText() ?: false
return _skiaLayer?.skikoView?.input?.hasText() ?: false
}
/**
......@@ -143,7 +203,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* @param text A string object representing the character typed on the system keyboard.
*/
override fun insertText(text: String) {
skiaLayer?.skikoView?.input?.insertText(text)
_skiaLayer?.skikoView?.input?.insertText(text)
}
/**
......@@ -152,7 +212,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* https://developer.apple.com/documentation/uikit/uikeyinput/1614572-deletebackward
*/
override fun deleteBackward() {
skiaLayer?.skikoView?.input?.deleteBackward()
_skiaLayer?.skikoView?.input?.deleteBackward()
}
override fun canBecomeFirstResponder() = true
......@@ -162,7 +222,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
for (press in withEvent.allPresses) {
val uiPress = press as? UIPress
if (uiPress != null) {
skiaLayer?.skikoView?.onKeyboardEvent(
_skiaLayer?.skikoView?.onKeyboardEvent(
toSkikoKeyboardEvent(press, SkikoKeyboardEventKind.DOWN)
)
}
......@@ -176,7 +236,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
for (press in withEvent.allPresses) {
val uiPress = press as? UIPress
if (uiPress != null) {
skiaLayer?.skikoView?.onKeyboardEvent(
_skiaLayer?.skikoView?.onKeyboardEvent(
toSkikoKeyboardEvent(press, SkikoKeyboardEventKind.UP)
)
}
......@@ -193,23 +253,10 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
return _pointInside(skiaPoint, withEvent)
}
/*
* When there at least one tracked touch, we need notify redrawer about it. It should schedule CADisplayLink which
* affects frequency of polling UITouch events on high frequency display and forces it to match display refresh rate.
*/
private var touchesCount = 0
set(value) {
field = value
val needHighFrequencyPolling = value > 0
skiaLayer?.redrawer?.needsProactiveDisplayLink = needHighFrequencyPolling
}
override fun touchesBegan(touches: Set<*>, withEvent: UIEvent?) {
super.touchesBegan(touches, withEvent)
touchesCount += touches.size
_touchesCount += touches.size
sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.DOWN)
}
......@@ -217,7 +264,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
override fun touchesEnded(touches: Set<*>, withEvent: UIEvent?) {
super.touchesEnded(touches, withEvent)
touchesCount -= touches.size
_touchesCount -= touches.size
sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.UP)
}
......@@ -230,7 +277,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
override fun touchesCancelled(touches: Set<*>, withEvent: UIEvent?) {
super.touchesCancelled(touches, withEvent)
touchesCount -= touches.size
_touchesCount -= touches.size
sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.UP)
}
......@@ -249,7 +296,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
)
}
skiaLayer?.skikoView?.onPointerEvent(
_skiaLayer?.skikoView?.onPointerEvent(
SkikoPointerEvent(
x = pointers.centroidX,
y = pointers.centroidY,
......@@ -261,10 +308,6 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
)
}
private val UITouch.isPressed get() =
phase != UITouchPhase.UITouchPhaseEnded &&
phase != UITouchPhase.UITouchPhaseCancelled
override fun inputDelegate(): UITextInputDelegateProtocol? {
return _inputDelegate
}
......@@ -280,7 +323,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* @return A substring of a document that falls within the specified range.
*/
override fun textInRange(range: UITextRange): String? {
return skiaLayer?.skikoView?.input?.textInRange(range.toIntRange())
return _skiaLayer?.skikoView?.input?.textInRange(range.toIntRange())
}
/**
......@@ -290,11 +333,11 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* @param withText A string to replace the text in range.
*/
override fun replaceRange(range: UITextRange, withText: String) {
skiaLayer?.skikoView?.input?.replaceRange(range.toIntRange(), withText)
_skiaLayer?.skikoView?.input?.replaceRange(range.toIntRange(), withText)
}
override fun setSelectedTextRange(selectedTextRange: UITextRange?) {
skiaLayer?.skikoView?.input?.setSelectedTextRange(selectedTextRange?.toIntRange())
_skiaLayer?.skikoView?.input?.setSelectedTextRange(selectedTextRange?.toIntRange())
}
/**
......@@ -305,7 +348,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* https://developer.apple.com/documentation/uikit/uitextinput/1614541-selectedtextrange
*/
override fun selectedTextRange(): UITextRange? {
return skiaLayer?.skikoView?.input?.getSelectedTextRange()?.toUITextRange()
return _skiaLayer?.skikoView?.input?.getSelectedTextRange()?.toUITextRange()
}
/**
......@@ -317,7 +360,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* https://developer.apple.com/documentation/uikit/uitextinput/1614489-markedtextrange
*/
override fun markedTextRange(): UITextRange? {
return skiaLayer?.skikoView?.input?.markedTextRange()?.toUITextRange()
return _skiaLayer?.skikoView?.input?.markedTextRange()?.toUITextRange()
}
override fun setMarkedTextStyle(markedTextStyle: Map<Any?, *>?) {
......@@ -342,7 +385,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
location.toInt() to length.toInt()
}
val relativeTextRange = locationRelative until locationRelative + lengthRelative
skiaLayer?.skikoView?.input?.setMarkedText(markedText, relativeTextRange)
_skiaLayer?.skikoView?.input?.setMarkedText(markedText, relativeTextRange)
}
/**
......@@ -351,7 +394,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* https://developer.apple.com/documentation/uikit/uitextinput/1614512-unmarktext
*/
override fun unmarkText() {
skiaLayer?.skikoView?.input?.unmarkText()
_skiaLayer?.skikoView?.input?.unmarkText()
}
override fun beginningOfDocument(): UITextPosition {
......@@ -363,7 +406,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* https://developer.apple.com/documentation/uikit/uitextinput/1614555-endofdocument
*/
override fun endOfDocument(): UITextPosition {
return IntermediateTextPosition(skiaLayer?.skikoView?.input?.endOfDocument() ?: 0)
return IntermediateTextPosition(_skiaLayer?.skikoView?.input?.endOfDocument() ?: 0)
}
/**
......@@ -383,7 +426,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
*/
override fun positionFromPosition(position: UITextPosition, offset: NSInteger): UITextPosition? {
val p = (position as? IntermediateTextPosition)?.position ?: return null
val endOfDocument = skiaLayer?.skikoView?.input?.endOfDocument()
val endOfDocument = _skiaLayer?.skikoView?.input?.endOfDocument()
return if (endOfDocument != null) {
IntermediateTextPosition(max(min(p + offset, endOfDocument), 0))
} else {
......@@ -485,12 +528,16 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
when (action) {
NSSelectorFromString(UIResponderStandardEditActionsProtocol::copy.name + ":") ->
_currentTextMenuActions?.copy != null
NSSelectorFromString(UIResponderStandardEditActionsProtocol::cut.name + ":") ->
_currentTextMenuActions?.cut != null
NSSelectorFromString(UIResponderStandardEditActionsProtocol::paste.name + ":") ->
_currentTextMenuActions?.paste != null
NSSelectorFromString(UIResponderStandardEditActionsProtocol::selectAll.name + ":") ->
_currentTextMenuActions?.selectAll != null
else -> false
}
......@@ -500,7 +547,9 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
override fun textContentType(): UITextContentType? = _skikoUITextInputTrains.textContentType()
override fun isSecureTextEntry(): Boolean = _skikoUITextInputTrains.isSecureTextEntry()
override fun enablesReturnKeyAutomatically(): Boolean = _skikoUITextInputTrains.enablesReturnKeyAutomatically()
override fun autocapitalizationType(): UITextAutocapitalizationType = _skikoUITextInputTrains.autocapitalizationType()
override fun autocapitalizationType(): UITextAutocapitalizationType =
_skikoUITextInputTrains.autocapitalizationType()
override fun autocorrectionType(): UITextAutocorrectionType = _skikoUITextInputTrains.autocorrectionType()
override fun dictationRecognitionFailed() {
......@@ -574,3 +623,8 @@ private fun NSWritingDirection.directionToStr() =
UITextLayoutDirectionDown -> "Down"
else -> "unknown direction"
}
private val UITouch.isPressed
get() =
phase != UITouchPhase.UITouchPhaseEnded &&
phase != UITouchPhase.UITouchPhaseCancelled
package org.jetbrains.skiko
import kotlinx.cinterop.*
import platform.Foundation.*
import platform.UIKit.*
@ExportObjCClass
class SkikoViewController : UIViewController {
@OverrideInit
constructor() : super(nibName = null, bundle = null)
@OverrideInit
constructor(coder: NSCoder) : super(coder)
constructor(skikoUIView: SkikoUIView) : this() {
this.skikoUIView = skikoUIView
}
private var skikoUIView: SkikoUIView? = null
override fun loadView() {
if (skikoUIView == null) {
super.loadView()
} else {
this.view = skikoUIView!!.load()
}
}
override fun viewDidLoad() {
super.viewDidLoad()
skikoUIView?.showScreenKeyboard()
}
// viewDidUnload() is deprecated and not called.
override fun viewDidDisappear(animated: Boolean) {
skikoUIView?.detach()
}
}
package org.jetbrains.skiko.context
import kotlinx.cinterop.useContents
import org.jetbrains.skia.*
import org.jetbrains.skiko.RenderException
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.redrawer.MetalRedrawer
internal class MetalContextHandler(layer: SkiaLayer) : ContextHandler(layer, layer::draw) {
val metalRedrawer: MetalRedrawer
get() = layer.redrawer!!
override fun initContext(): Boolean {
try {
if (context == null) {
context = metalRedrawer.makeContext()
}
} catch (e: Exception) {
println("${e.message}\nFailed to create Skia Metal context!")
return false
}
return true
}
private var currentWidth = 0
private var currentHeight = 0
private fun isSizeChanged(width: Int, height: Int): Boolean {
if (width != currentWidth || height != currentHeight) {
currentWidth = width
currentHeight = height
return true
}
return false
}
override fun initCanvas() {
disposeCanvas()
val scale = layer.contentScale
val (w, h) = layer.view!!.frame.useContents {
(size.width * scale).toInt().coerceAtLeast(0) to (size.height * scale).toInt().coerceAtLeast(0)
}
if (isSizeChanged(w, h)) {
metalRedrawer.syncSize()
}
if (w > 0 && h > 0) {
renderTarget = metalRedrawer.makeRenderTarget(w, h)
surface = Surface.makeFromBackendRenderTarget(
context!!,
renderTarget!!,
SurfaceOrigin.TOP_LEFT,
SurfaceColorFormat.BGRA_8888,
ColorSpace.sRGB,
SurfaceProps(pixelGeometry = layer.pixelGeometry)
) ?: throw RenderException("Cannot create surface")
canvas = surface!!.canvas
} else {
renderTarget = null
surface = null
canvas = null
}
}
override fun flush() {
// TODO: maybe make flush async as in JVM version.
super.flush()
surface?.flushAndSubmit()
metalRedrawer.finishFrame()
}
override fun rendererInfo(): String {
return "Native Metal: device ${metalRedrawer.device.name}"
}
}
package org.jetbrains.skiko.redrawer
import kotlinx.cinterop.*
import org.jetbrains.skia.BackendRenderTarget
import org.jetbrains.skia.DirectContext
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.context.ContextHandler
import org.jetbrains.skiko.context.MetalContextHandler
import platform.CoreGraphics.CGColorCreate
import platform.CoreGraphics.CGColorSpaceCreateDeviceRGB
import platform.CoreGraphics.CGContextRef
import platform.CoreGraphics.CGSizeMake
import org.jetbrains.skia.*
import org.jetbrains.skiko.Logger
import platform.Foundation.NSRunLoop
import platform.Foundation.NSSelectorFromString
import platform.Metal.MTLCreateSystemDefaultDevice
import platform.Metal.MTLDeviceProtocol
import platform.Metal.MTLPixelFormatBGRA8Unorm
import platform.QuartzCore.*
import platform.darwin.*
import kotlin.math.roundToInt
internal class MetalRedrawer(
private val layer: SkiaLayer
) : Redrawer {
private val contextHandler = MetalContextHandler(layer)
override val renderInfo: String get() = contextHandler.rendererInfo()
private val metalLayer: CAMetalLayer,
private val drawIntoSurfaceCallback: (Surface) -> Unit
) {
private var isDisposed = false
internal val device = MTLCreateSystemDefaultDevice() ?: throw IllegalStateException("Metal is not supported on this system")
// Workaround for KN compiler bug
// Type mismatch: inferred type is objcnames.protocols.MTLDeviceProtocol but platform.Metal.MTLDeviceProtocol was expected
@Suppress("USELESS_CAST")
private val device = metalLayer.device as platform.Metal.MTLDeviceProtocol?
?: throw IllegalStateException("CAMetalLayer.device can not be null")
private val queue = device.newCommandQueue() ?: throw IllegalStateException("Couldn't create Metal command queue")
private var currentDrawable: CAMetalDrawableProtocol? = null
private val metalLayer = MetalLayer()
private val context = DirectContext.makeMetal(device.objcPtr(), queue.objcPtr())
// Semaphore for preventing command buffers count more than swapchain size to be scheduled/executed at the same time
private val inflightSemaphore = dispatch_semaphore_create(metalLayer.maximumDrawableCount.toLong())
var maximumFramesPerSecond: NSInteger
get() = caDisplayLink.preferredFramesPerSecond
set(value) {
caDisplayLink.preferredFramesPerSecond = value
}
/*
* Indicates that scene is invalidated and next display link callback will draw
*/
......@@ -55,7 +54,7 @@ internal class MetalRedrawer(
if (hasScheduledDrawOnNextVSync) {
hasScheduledDrawOnNextVSync = false
drawIfLayerIsShowing()
draw()
}
if (!needsProactiveDisplayLink) {
......@@ -67,47 +66,20 @@ internal class MetalRedrawer(
target = frameListener,
selector = NSSelectorFromString(FrameTickListener::onDisplayLinkTick.name)
)
init {
metalLayer.init(this.layer, contextHandler, device)
caDisplayLink.setPaused(true)
caDisplayLink.addToRunLoop(NSRunLoop.mainRunLoop, NSRunLoop.mainRunLoop.currentMode)
}
fun makeContext() = DirectContext.makeMetal(device.objcPtr(), queue.objcPtr())
fun makeRenderTarget(width: Int, height: Int): BackendRenderTarget {
// If more than swapchain size count of command buffers are inflight
// wait until one finishes work
dispatch_semaphore_wait(inflightSemaphore, DISPATCH_TIME_FOREVER)
currentDrawable = metalLayer.nextDrawable()!!
return BackendRenderTarget.makeMetal(width, height, currentDrawable!!.texture.objcPtr())
}
override fun dispose() {
internal fun dispose() {
if (!isDisposed) {
caDisplayLink.invalidate()
contextHandler.dispose()
metalLayer.dispose()
isDisposed = true
}
}
override fun syncSize() {
metalLayer.contentsScale = layer.contentScale.toDouble()
val osView = layer.view!!
val (w, h) = osView.frame.useContents {
size.width to size.height
}
metalLayer.frame = osView.frame
metalLayer.init(layer, contextHandler, device)
metalLayer.drawableSize = CGSizeMake(w * metalLayer.contentsScale, h * metalLayer.contentsScale)
osView.window?.screen?.maximumFramesPerSecond?.let {
caDisplayLink.preferredFramesPerSecond = it
}
}
override fun needRedraw() {
internal fun needRedraw() {
check(!isDisposed) { "MetalRedrawer is disposed" }
hasScheduledDrawOnNextVSync = true
......@@ -116,86 +88,66 @@ internal class MetalRedrawer(
caDisplayLink.setPaused(false)
}
override fun redrawImmediately() {
// TODO: separate iOS MetalRedrawer from Redrawer, it's a false abstraction, iOS only uses MetalRedrawer explicitly
throw UnsupportedOperationException("This should never be called on iOS")
private fun draw() {
if (isDisposed) {
return
}
private fun drawIfLayerIsShowing() {
if (layer.isShowing()) {
draw()
}
autoreleasepool {
val (width, height) = metalLayer.drawableSize.useContents {
width.roundToInt() to height.roundToInt()
}
private fun draw() {
// TODO: maybe make flush async as in JVM version.
autoreleasepool { //todo measure performance without autoreleasepool
if (!isDisposed) {
contextHandler.draw()
if (width <= 0 || height <= 0) {
return@autoreleasepool
}
dispatch_semaphore_wait(inflightSemaphore, DISPATCH_TIME_FOREVER)
val metalDrawable = metalLayer.nextDrawable()
if (metalDrawable == null) {
Logger.warn { "'metalLayer.nextDrawable()' returned null. 'metalLayer.allowsNextDrawableTimeout' should be set to false. Skipping the frame." }
dispatch_semaphore_signal(inflightSemaphore)
return@autoreleasepool
}
val renderTarget = BackendRenderTarget.makeMetal(width, height, metalDrawable.texture.objcPtr())
val surface = Surface.makeFromBackendRenderTarget(
context,
renderTarget,
SurfaceOrigin.TOP_LEFT,
SurfaceColorFormat.BGRA_8888,
ColorSpace.sRGB,
SurfaceProps(pixelGeometry = PixelGeometry.UNKNOWN)
)
if (surface == null) {
Logger.warn { "'Surface.makeFromBackendRenderTarget' returned null. Skipping the frame." }
renderTarget.close()
// TODO: manually release metalDrawable when K/N API arrives
dispatch_semaphore_signal(inflightSemaphore)
return@autoreleasepool
}
fun finishFrame() {
autoreleasepool {
currentDrawable?.let {
surface.canvas.clear(Color.WHITE)
drawIntoSurfaceCallback(surface)
surface.flushAndSubmit()
val commandBuffer = queue.commandBuffer()!!
commandBuffer.label = "Present"
commandBuffer.presentDrawable(it)
commandBuffer.presentDrawable(metalDrawable)
commandBuffer.addCompletedHandler {
// Signal work finish, allow a new command buffer to be scheduled
dispatch_semaphore_signal(inflightSemaphore)
}
commandBuffer.commit()
currentDrawable = null
}
}
}
}
internal class MetalLayer : CAMetalLayer {
private lateinit var skiaLayer: SkiaLayer
private lateinit var contextHandler: ContextHandler
@OverrideInit
constructor() : super()
@OverrideInit
constructor(layer: Any) : super(layer)
fun init(
skiaLayer: SkiaLayer,
contextHandler: ContextHandler,
theDevice: MTLDeviceProtocol
) {
this.skiaLayer = skiaLayer
this.contextHandler = contextHandler
this.setNeedsDisplayOnBoundsChange(true)
this.removeAllAnimations()
// TODO: looks like a bug in K/N interop.
this.device = theDevice as objcnames.protocols.MTLDeviceProtocol?
this.pixelFormat = MTLPixelFormatBGRA8Unorm
this.contentsGravity = kCAGravityTopLeft
doubleArrayOf(0.0, 0.0, 0.0, 0.0).usePinned {
this.backgroundColor =
CGColorCreate(CGColorSpaceCreateDeviceRGB(), it.addressOf(0))
}
this.framebufferOnly = false
this.opaque = false // For UIKit interop through a "Hole"
skiaLayer.view?.let {
this.frame = it.frame
it.layer.addSublayer(this)
}
}
fun dispose() {
this.removeFromSuperlayer()
// TODO: anything else to dispose the layer?
}
override fun drawInContext(ctx: CGContextRef?) {
contextHandler.draw()
super.drawInContext(ctx)
surface.close()
renderTarget.close()
// TODO manually release metalDrawable when K/N API arrives
}
}
}
......
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